diff --git a/.github/workflows/pr-python.yml b/.github/workflows/pr-python.yml index 81bbce894..c284f5d78 100644 --- a/.github/workflows/pr-python.yml +++ b/.github/workflows/pr-python.yml @@ -31,6 +31,23 @@ jobs: "examples/**.py" ]' + black_latest_3_11: + needs: pre_job + if: ${{ needs.pre_job.outputs.should_skip != 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + with: + python-version: '3.11' + + - name: install mypy + run: | + pip install -r requirements.txt + pip install black + - name: black + run: black --check . + mypy_latest_3_11: needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} @@ -49,21 +66,6 @@ jobs: - name: mypy run: mypy --install-types . - unittest_latest_3_11: - needs: pre_job - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 - with: - python-version: '3.11' - - - name: install dependencies - run: pip install -r requirements.txt - - name: unittest - run: python -m unittest discover - packagetest_latest_3_11: needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} @@ -83,23 +85,20 @@ jobs: - name: toptest run: cd examples && python test_blinky.py - mypy_latest_3_9: + unittest_latest_3_11: needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 - with: - python-version: '3.9' + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + with: + python-version: '3.11' - - name: install mypy - run: | - pip install -r requirements.txt - pip install mypy mypy-protobuf - mypy --version - - name: mypy - run: mypy --install-types . + - name: install dependencies + run: pip install -r requirements.txt + - name: unittest + run: python -m unittest discover unittest_latest_3_9: needs: pre_job diff --git a/blinky_skeleton.py b/blinky_skeleton.py index a5a463d2a..e398f0186 100644 --- a/blinky_skeleton.py +++ b/blinky_skeleton.py @@ -4,11 +4,11 @@ class BlinkyExample(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - # your implementation here + @override + def contents(self) -> None: + super().contents() + # your implementation here if __name__ == "__main__": - compile_board_inplace(BlinkyExample) + compile_board_inplace(BlinkyExample) diff --git a/edg/BoardCompiler.py b/edg/BoardCompiler.py index c3bf8e422..b0735a838 100644 --- a/edg/BoardCompiler.py +++ b/edg/BoardCompiler.py @@ -11,64 +11,65 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]]) -> CompiledDesign: - if target_dir_name is not None: - (target_dir, target_name) = target_dir_name - if not os.path.exists(target_dir): - os.makedirs(target_dir) - assert os.path.isdir(target_dir), f"target_dir {target_dir} to compile_board must be directory" + if target_dir_name is not None: + (target_dir, target_name) = target_dir_name + if not os.path.exists(target_dir): + os.makedirs(target_dir) + assert os.path.isdir(target_dir), f"target_dir {target_dir} to compile_board must be directory" - design_filename = os.path.join(target_dir, f'{target_name}.edg') - netlist_filename = os.path.join(target_dir, f'{target_name}.net') - bom_filename = os.path.join(target_dir, f'{target_name}.csv') - svgpcb_filename = os.path.join(target_dir, f'{target_name}.svgpcb.js') + design_filename = os.path.join(target_dir, f"{target_name}.edg") + netlist_filename = os.path.join(target_dir, f"{target_name}.net") + bom_filename = os.path.join(target_dir, f"{target_name}.csv") + svgpcb_filename = os.path.join(target_dir, f"{target_name}.svgpcb.js") - with suppress(FileNotFoundError): - os.remove(design_filename) - with suppress(FileNotFoundError): - os.remove(netlist_filename) - with suppress(FileNotFoundError): - os.remove(bom_filename) - with suppress(FileNotFoundError): - os.remove(svgpcb_filename) + with suppress(FileNotFoundError): + os.remove(design_filename) + with suppress(FileNotFoundError): + os.remove(netlist_filename) + with suppress(FileNotFoundError): + os.remove(bom_filename) + with suppress(FileNotFoundError): + os.remove(svgpcb_filename) - compiled = ScalaCompiler.compile(design, ignore_errors=True) - compiled.append_values(RefdesRefinementPass().run(compiled)) + compiled = ScalaCompiler.compile(design, ignore_errors=True) + compiled.append_values(RefdesRefinementPass().run(compiled)) - if target_dir_name is not None: # always dump the proto even if there is an error - with open(design_filename, 'wb') as raw_file: - raw_file.write(compiled.design.SerializeToString()) + if target_dir_name is not None: # always dump the proto even if there is an error + with open(design_filename, "wb") as raw_file: + raw_file.write(compiled.design.SerializeToString()) - if compiled.errors: - from . import core - raise core.ScalaCompilerInterface.CompilerCheckError(f"error during compilation:\n{compiled.errors_str()}") + if compiled.errors: + from . import core - netlist_all = NetlistBackend().run(compiled) - bom_all = GenerateBom().run(compiled) - svgpcb_all = SvgPcbBackend().run(compiled) - assert len(netlist_all) == 1 + raise core.ScalaCompilerInterface.CompilerCheckError(f"error during compilation:\n{compiled.errors_str()}") - if target_dir_name is not None: - with open(netlist_filename, 'w', encoding='utf-8') as net_file: - net_file.write(netlist_all[0][1]) + netlist_all = NetlistBackend().run(compiled) + bom_all = GenerateBom().run(compiled) + svgpcb_all = SvgPcbBackend().run(compiled) + assert len(netlist_all) == 1 - with open(bom_filename, 'w', encoding='utf-8') as bom_file: - bom_file.write(bom_all[0][1]) + if target_dir_name is not None: + with open(netlist_filename, "w", encoding="utf-8") as net_file: + net_file.write(netlist_all[0][1]) - if svgpcb_all: - with open(svgpcb_filename, 'w', encoding='utf-8') as bom_file: - bom_file.write(svgpcb_all[0][1]) + with open(bom_filename, "w", encoding="utf-8") as bom_file: + bom_file.write(bom_all[0][1]) - return compiled + if svgpcb_all: + with open(svgpcb_filename, "w", encoding="utf-8") as bom_file: + bom_file.write(svgpcb_all[0][1]) + + return compiled def compile_board_inplace(design: Type[Block], generate: bool = True) -> CompiledDesign: - """Compiles a board and writes the results in a sub-directory - where the module containing the top-level is located""" - designfile = inspect.getfile(design) - if generate: - target_dir_name = (os.path.join(os.path.dirname(designfile), design.__name__), design.__name__) - else: - target_dir_name = None - compiled = compile_board(design, target_dir_name) - - return compiled + """Compiles a board and writes the results in a sub-directory + where the module containing the top-level is located""" + designfile = inspect.getfile(design) + if generate: + target_dir_name = (os.path.join(os.path.dirname(designfile), design.__name__), design.__name__) + else: + target_dir_name = None + compiled = compile_board(design, target_dir_name) + + return compiled diff --git a/edg/BoardTop.py b/edg/BoardTop.py index 65e374b72..666029c3f 100644 --- a/edg/BoardTop.py +++ b/edg/BoardTop.py @@ -4,135 +4,134 @@ class BaseBoardTop(DesignTop): - """Design top with refinements for intermediate-level (0603+ SMD), hand-solderable components.""" - def __init__(self) -> None: - super().__init__() - self.refdes_prefix = self.Parameter(StringExpr()) - self.assign(self.refdes_prefix, "") # override with refinements - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - class_refinements=[ - (Resistor, GenericChipResistor), - (ResistorArray, JlcResistorArray), # TODO: replace with generic resistor array - (Capacitor, GenericMlcc), - (Inductor, JlcInductor), # TODO: replace with generic inductor - (Switch, SmtSwitch), - (RotaryEncoder, Ec11j15WithSwitch), - (Diode, JlcDiode), # TODO: replace with non-distributor parts list - (ZenerDiode, JlcZenerDiode), # TODO: replace with non-distributor parts list - (Bjt, JlcBjt), # TODO: replace with non-distributor parts list - (Fet, JlcFet), # TODO: replace with non-distributor parts list - (SwitchFet, JlcSwitchFet), # TODO: replace with non-distributor parts list - (Led, SmtLed), - (RgbLedCommonAnode, Smt0606RgbLed), - (Crystal, JlcCrystal), # TODO: replace with non-distributor parts list - (Oscillator, JlcOscillator), # TODO: replace with non-distributor parts list - - (Jumper, SolderJumperTriangular), - (IndicatorSinkLed, IndicatorSinkLedResistor), - - (Fpc050Bottom, HiroseFh12sh), - (UsbEsdDiode, Tpd2e009), - (CanEsdDiode, Pesd1can), - (Fuse, Nano2Fuseholder), - (TestPoint, TeRc), - - (SwdCortexTargetConnector, SwdCortexTargetHeader), - - (SpiMemory, W25q), - - (Speaker, ConnectorSpeaker), - ], class_values=[ - (SelectorArea, ['footprint_area'], Range.from_lower(4.0)), # at least 0603 - ] - ) + """Design top with refinements for intermediate-level (0603+ SMD), hand-solderable components.""" + + def __init__(self) -> None: + super().__init__() + self.refdes_prefix = self.Parameter(StringExpr()) + self.assign(self.refdes_prefix, "") # override with refinements + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + class_refinements=[ + (Resistor, GenericChipResistor), + (ResistorArray, JlcResistorArray), # TODO: replace with generic resistor array + (Capacitor, GenericMlcc), + (Inductor, JlcInductor), # TODO: replace with generic inductor + (Switch, SmtSwitch), + (RotaryEncoder, Ec11j15WithSwitch), + (Diode, JlcDiode), # TODO: replace with non-distributor parts list + (ZenerDiode, JlcZenerDiode), # TODO: replace with non-distributor parts list + (Bjt, JlcBjt), # TODO: replace with non-distributor parts list + (Fet, JlcFet), # TODO: replace with non-distributor parts list + (SwitchFet, JlcSwitchFet), # TODO: replace with non-distributor parts list + (Led, SmtLed), + (RgbLedCommonAnode, Smt0606RgbLed), + (Crystal, JlcCrystal), # TODO: replace with non-distributor parts list + (Oscillator, JlcOscillator), # TODO: replace with non-distributor parts list + (Jumper, SolderJumperTriangular), + (IndicatorSinkLed, IndicatorSinkLedResistor), + (Fpc050Bottom, HiroseFh12sh), + (UsbEsdDiode, Tpd2e009), + (CanEsdDiode, Pesd1can), + (Fuse, Nano2Fuseholder), + (TestPoint, TeRc), + (SwdCortexTargetConnector, SwdCortexTargetHeader), + (SpiMemory, W25q), + (Speaker, ConnectorSpeaker), + ], + class_values=[ + (SelectorArea, ["footprint_area"], Range.from_lower(4.0)), # at least 0603 + ], + ) class BoardTop(BaseBoardTop): - pass + pass class JlcToolingHole(InternalSubcircuit, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'H', 'edg:JlcToolingHole_1.152mm', - {}, - datasheet='https://support.jlcpcb.com/article/92-how-to-add-tooling-holes-for-smt-assembly-order' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "H", + "edg:JlcToolingHole_1.152mm", + {}, + datasheet="https://support.jlcpcb.com/article/92-how-to-add-tooling-holes-for-smt-assembly-order", + ) class JlcToolingHoles(InternalSubcircuit, Block): - @override - def contents(self) -> None: - super().contents() - self.th1 = self.Block(JlcToolingHole()) - self.th2 = self.Block(JlcToolingHole()) - self.th3 = self.Block(JlcToolingHole()) + @override + def contents(self) -> None: + super().contents() + self.th1 = self.Block(JlcToolingHole()) + self.th2 = self.Block(JlcToolingHole()) + self.th3 = self.Block(JlcToolingHole()) class JlcTopRefinements(BaseBoardTop): - """Design top with refinements to use parts from JLC's assembly service""" - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - class_refinements=[ - (Resistor, JlcResistor), - (Capacitor, JlcCapacitor), - (Inductor, JlcInductor), - (AluminumCapacitor, JlcAluminumCapacitor), - (FerriteBead, JlcFerriteBead), - (PptcFuse, JlcPptcFuse), - (ResistorArray, JlcResistorArray), - (Crystal, JlcCrystal), - (Oscillator, JlcOscillator), - - (Switch, JlcSwitch), - (Led, JlcLed), - (ZenerDiode, JlcZenerDiode), - (Diode, JlcDiode), - (Bjt, JlcBjt), - (Fet, JlcFet), - (Antenna, JlcAntenna), - - (Fpc050Bottom, Afc01), - (Fpc050Top, Afc07Top), - (Fpc030Bottom, HiroseFh35cshw), - (UsbEsdDiode, Pesd5v0x1bt), - (Comparator, Lmv331), - (Opamp, Lmv321), - (SpiMemory, W25q), # 128M version is a basic part - (TestPoint, Keystone5015), # this is larger, but is part of JLC's parts inventory - (UflConnector, Bwipx_1_001e), - ], - class_values=[ # realistically only RCs are going to likely be basic parts - (JlcResistor, ['require_basic_part'], True), - (JlcCapacitor, ['require_basic_part'], True), - ], - ) + """Design top with refinements to use parts from JLC's assembly service""" + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + class_refinements=[ + (Resistor, JlcResistor), + (Capacitor, JlcCapacitor), + (Inductor, JlcInductor), + (AluminumCapacitor, JlcAluminumCapacitor), + (FerriteBead, JlcFerriteBead), + (PptcFuse, JlcPptcFuse), + (ResistorArray, JlcResistorArray), + (Crystal, JlcCrystal), + (Oscillator, JlcOscillator), + (Switch, JlcSwitch), + (Led, JlcLed), + (ZenerDiode, JlcZenerDiode), + (Diode, JlcDiode), + (Bjt, JlcBjt), + (Fet, JlcFet), + (Antenna, JlcAntenna), + (Fpc050Bottom, Afc01), + (Fpc050Top, Afc07Top), + (Fpc030Bottom, HiroseFh35cshw), + (UsbEsdDiode, Pesd5v0x1bt), + (Comparator, Lmv331), + (Opamp, Lmv321), + (SpiMemory, W25q), # 128M version is a basic part + (TestPoint, Keystone5015), # this is larger, but is part of JLC's parts inventory + (UflConnector, Bwipx_1_001e), + ], + class_values=[ # realistically only RCs are going to likely be basic parts + (JlcResistor, ["require_basic_part"], True), + (JlcCapacitor, ["require_basic_part"], True), + ], + ) class JlcBoardTop(JlcTopRefinements): - """Design top with refinements to use parts from JLC's assembly service and including the tooling holes""" - @override - def contents(self) -> None: - super().contents() - self.jlc_th = self.Block(JlcToolingHoles()) + """Design top with refinements to use parts from JLC's assembly service and including the tooling holes""" + + @override + def contents(self) -> None: + super().contents() + self.jlc_th = self.Block(JlcToolingHoles()) class SimpleBoardTop(JlcTopRefinements): - """A BoardTop with refinements that make getting started easier but may not be desirable everywhere.""" - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - class_refinements=[ - (PassiveConnector, PinHeader254), - ], - class_values=[ - (Nonstrict3v3Compatible, ['nonstrict_3v3_compatible'], True), - (JlcInductor, ['manual_frequency_rating'], Range.all()), - ], - ) + """A BoardTop with refinements that make getting started easier but may not be desirable everywhere.""" + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + class_refinements=[ + (PassiveConnector, PinHeader254), + ], + class_values=[ + (Nonstrict3v3Compatible, ["nonstrict_3v3_compatible"], True), + (JlcInductor, ["manual_frequency_rating"], Range.all()), + ], + ) diff --git a/edg/abstract_parts/AbstractAnalogSwitch.py b/edg/abstract_parts/AbstractAnalogSwitch.py index e5156d560..b31375ee8 100644 --- a/edg/abstract_parts/AbstractAnalogSwitch.py +++ b/edg/abstract_parts/AbstractAnalogSwitch.py @@ -8,198 +8,215 @@ @abstract_block class AnalogSwitch(Interface, KiCadImportableBlock, Block): - """Base class for a n-ported analog switch with passive-typed ports.""" - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name.startswith('edg_importable:Mux') # can be any Mux - count = int(symbol_name.removeprefix('edg_importable:Mux')) - pins: Dict[str, BasePort] = { - 'C': self.com, 'S': self.control, 'V+': self.pwr, 'V-': self.gnd - } - pins.update({str(i+1): self.inputs.request() for i in range(count)}) - return pins + """Base class for a n-ported analog switch with passive-typed ports.""" - def __init__(self) -> None: - super().__init__() + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name.startswith("edg_importable:Mux") # can be any Mux + count = int(symbol_name.removeprefix("edg_importable:Mux")) + pins: Dict[str, BasePort] = {"C": self.com, "S": self.control, "V+": self.pwr, "V-": self.gnd} + pins.update({str(i + 1): self.inputs.request() for i in range(count)}) + return pins - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) + def __init__(self) -> None: + super().__init__() - self.control_gnd = self.Port(Ground.empty(), optional=True) # optional separate ground for control signal - self.control = self.Port(Vector(DigitalSink.empty())) # length source + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.com = self.Port(Passive.empty()) - self.inputs = self.Port(Vector(Passive.empty())) + self.control_gnd = self.Port(Ground.empty(), optional=True) # optional separate ground for control signal + self.control = self.Port(Vector(DigitalSink.empty())) # length source - # TODO: this is a different way of modeling parts - parameters in the part itself - # instead of on the ports (because this doesn't have typed ports) - self.analog_voltage_limits = self.Parameter(RangeExpr()) # for all analog ports - self.analog_current_limits = self.Parameter(RangeExpr()) # for all analog ports - self.analog_on_resistance = self.Parameter(RangeExpr()) + self.com = self.Port(Passive.empty()) + self.inputs = self.Port(Vector(Passive.empty())) + + # TODO: this is a different way of modeling parts - parameters in the part itself + # instead of on the ports (because this doesn't have typed ports) + self.analog_voltage_limits = self.Parameter(RangeExpr()) # for all analog ports + self.analog_current_limits = self.Parameter(RangeExpr()) # for all analog ports + self.analog_on_resistance = self.Parameter(RangeExpr()) class AnalogSwitchTree(AnalogSwitch, GeneratorBlock): - """Generates an n-ported analog switch by creating a tree of individual, smaller switches. - Parameterized by the size of the element switches.""" - def __init__(self, switch_size: IntLike = 0): - super().__init__() - self.switch_size = self.ArgParameter(switch_size) - self.generator_param(self.switch_size, self.inputs.requested(), self.control_gnd.is_connected()) - - @override - def generate(self) -> None: - import math - super().generate() - - switch_size = self.get(self.switch_size) - elts = self.get(self.inputs.requested()) - assert switch_size > 1, f"switch size {switch_size} must be greater than 1" - assert len(elts) > 1, "passthrough AnalogSwitchTree not (yet?) supported" - self.sw = ElementDict[AnalogSwitch]() - - self.inputs.defined() - self.control.defined() - - stage_num_controls = math.ceil(math.log2(switch_size)) # number of control IOs per stage - ports = [self.inputs.append_elt(Passive.empty(), str(i)) for i in range(len(elts))] - all_switches = [] - switch_stage = 0 - - while len(ports) > 1: # stages in the tree - num_switches = math.ceil(len(ports) / switch_size) - new_ports = [] # output ports of this current stage - - stage_control_ios = [self.control.append_elt(DigitalSink.empty(), f'{switch_stage}_{i}') - for i in range(stage_num_controls)] - - for sw_i in range(num_switches): - sw = self.sw[f'{switch_stage}_{sw_i}'] = self.Block(AnalogSwitch()) - all_switches.append(sw) - self.connect(sw.pwr, self.pwr) - self.connect(sw.gnd, self.gnd) - if self.get(self.control_gnd.is_connected()): - self.connect(sw.control_gnd, self.control_gnd) - - for sw_port_i in range(switch_size): - port_i = sw_i * switch_size + sw_port_i - if port_i < len(ports): - self.connect(sw.inputs.request(str(sw_port_i)), ports[port_i]) - new_ports.append(sw.com) - - for (i, control_io) in enumerate(stage_control_ios): - self.connect(sw.control.request(str(i)), control_io) - - ports = new_ports - switch_stage += 1 - - assert len(ports) == 1 - self.connect(ports[0], self.com) - - # Create bulk tree model - # Voltage is unchanged - self.assign(self.analog_voltage_limits, all_switches[0].analog_voltage_limits) - # Current limit is bottlenecked by the final stage - self.assign(self.analog_current_limits, - all_switches[0].analog_current_limits / (switch_size ** (switch_stage - 1))) - # On resistance sums through each stage - self.assign(self.analog_on_resistance, all_switches[0].analog_on_resistance * switch_stage) + """Generates an n-ported analog switch by creating a tree of individual, smaller switches. + Parameterized by the size of the element switches.""" + + def __init__(self, switch_size: IntLike = 0): + super().__init__() + self.switch_size = self.ArgParameter(switch_size) + self.generator_param(self.switch_size, self.inputs.requested(), self.control_gnd.is_connected()) + + @override + def generate(self) -> None: + import math + + super().generate() + + switch_size = self.get(self.switch_size) + elts = self.get(self.inputs.requested()) + assert switch_size > 1, f"switch size {switch_size} must be greater than 1" + assert len(elts) > 1, "passthrough AnalogSwitchTree not (yet?) supported" + self.sw = ElementDict[AnalogSwitch]() + + self.inputs.defined() + self.control.defined() + + stage_num_controls = math.ceil(math.log2(switch_size)) # number of control IOs per stage + ports = [self.inputs.append_elt(Passive.empty(), str(i)) for i in range(len(elts))] + all_switches = [] + switch_stage = 0 + + while len(ports) > 1: # stages in the tree + num_switches = math.ceil(len(ports) / switch_size) + new_ports = [] # output ports of this current stage + + stage_control_ios = [ + self.control.append_elt(DigitalSink.empty(), f"{switch_stage}_{i}") for i in range(stage_num_controls) + ] + + for sw_i in range(num_switches): + sw = self.sw[f"{switch_stage}_{sw_i}"] = self.Block(AnalogSwitch()) + all_switches.append(sw) + self.connect(sw.pwr, self.pwr) + self.connect(sw.gnd, self.gnd) + if self.get(self.control_gnd.is_connected()): + self.connect(sw.control_gnd, self.control_gnd) + + for sw_port_i in range(switch_size): + port_i = sw_i * switch_size + sw_port_i + if port_i < len(ports): + self.connect(sw.inputs.request(str(sw_port_i)), ports[port_i]) + new_ports.append(sw.com) + + for i, control_io in enumerate(stage_control_ios): + self.connect(sw.control.request(str(i)), control_io) + + ports = new_ports + switch_stage += 1 + + assert len(ports) == 1 + self.connect(ports[0], self.com) + + # Create bulk tree model + # Voltage is unchanged + self.assign(self.analog_voltage_limits, all_switches[0].analog_voltage_limits) + # Current limit is bottlenecked by the final stage + self.assign( + self.analog_current_limits, all_switches[0].analog_current_limits / (switch_size ** (switch_stage - 1)) + ) + # On resistance sums through each stage + self.assign(self.analog_on_resistance, all_switches[0].analog_on_resistance * switch_stage) class AnalogMuxer(Interface, KiCadImportableBlock, GeneratorBlock): - """Wrapper around AnalogSwitch that provides muxing functionality - multiple sink ports, one source port. - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name.startswith('edg_importable:Mux') # can be any Mux - count = int(symbol_name.removeprefix('edg_importable:Mux')) - pins: Dict[str, BasePort] = { - 'C': self.out, 'S': self.control, 'V+': self.pwr, 'V-': self.gnd - } - pins.update({str(i+1): self.inputs.request(str(i)) for i in range(count)}) - return pins - - def __init__(self) -> None: - super().__init__() - self.device = self.Block(AnalogSwitch()) - self.pwr = self.Export(self.device.pwr, [Power]) - self.gnd = self.Export(self.device.gnd, [Common]) - - self.control_gnd = self.Port(Ground.empty(), optional=True) # optional separate ground for control signal - self.control = self.Export(self.device.control) - - self.inputs = self.Port(Vector(AnalogSink.empty())) - self.out = self.Export(self.device.com.adapt_to(AnalogSource( - voltage_out=self.inputs.hull(lambda x: x.link().voltage), - signal_out=self.inputs.hull(lambda x: x.link().signal), - current_limits=self.device.analog_current_limits, # this device only, current draw propagated - impedance=self.device.analog_on_resistance + self.inputs.hull(lambda x: x.link().source_impedance) - ))) - - self.generator_param(self.inputs.requested(), self.control_gnd.is_connected()) - - @override - def generate(self) -> None: - super().generate() - self.inputs.defined() - for elt in self.get(self.inputs.requested()): - self.connect( - self.inputs.append_elt(AnalogSink.empty(), elt), - self.device.inputs.request(elt).adapt_to(AnalogSink( - voltage_limits=self.device.analog_voltage_limits, # this device only, voltages propagated - current_draw=self.out.link().current_drawn, - impedance=self.out.link().sink_impedance + self.device.analog_on_resistance - ))) - if self.get(self.control_gnd.is_connected()): - self.connect(self.control_gnd, self.device.control_gnd) - - def mux_to(self, inputs: Optional[List[Port[AnalogLink]]] = None, - output: Optional[Port[AnalogLink]] = None) -> 'AnalogMuxer': - if inputs is not None: - for i, input_port in enumerate(inputs): - cast(Block, builder.get_enclosing_block()).connect(input_port, self.inputs.request(str(i))) - if output is not None: - cast(Block, builder.get_enclosing_block()).connect(output, self.out) - return self + """Wrapper around AnalogSwitch that provides muxing functionality - multiple sink ports, one source port.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name.startswith("edg_importable:Mux") # can be any Mux + count = int(symbol_name.removeprefix("edg_importable:Mux")) + pins: Dict[str, BasePort] = {"C": self.out, "S": self.control, "V+": self.pwr, "V-": self.gnd} + pins.update({str(i + 1): self.inputs.request(str(i)) for i in range(count)}) + return pins + + def __init__(self) -> None: + super().__init__() + self.device = self.Block(AnalogSwitch()) + self.pwr = self.Export(self.device.pwr, [Power]) + self.gnd = self.Export(self.device.gnd, [Common]) + + self.control_gnd = self.Port(Ground.empty(), optional=True) # optional separate ground for control signal + self.control = self.Export(self.device.control) + + self.inputs = self.Port(Vector(AnalogSink.empty())) + self.out = self.Export( + self.device.com.adapt_to( + AnalogSource( + voltage_out=self.inputs.hull(lambda x: x.link().voltage), + signal_out=self.inputs.hull(lambda x: x.link().signal), + current_limits=self.device.analog_current_limits, # this device only, current draw propagated + impedance=self.device.analog_on_resistance + self.inputs.hull(lambda x: x.link().source_impedance), + ) + ) + ) + + self.generator_param(self.inputs.requested(), self.control_gnd.is_connected()) + + @override + def generate(self) -> None: + super().generate() + self.inputs.defined() + for elt in self.get(self.inputs.requested()): + self.connect( + self.inputs.append_elt(AnalogSink.empty(), elt), + self.device.inputs.request(elt).adapt_to( + AnalogSink( + voltage_limits=self.device.analog_voltage_limits, # this device only, voltages propagated + current_draw=self.out.link().current_drawn, + impedance=self.out.link().sink_impedance + self.device.analog_on_resistance, + ) + ), + ) + if self.get(self.control_gnd.is_connected()): + self.connect(self.control_gnd, self.device.control_gnd) + + def mux_to( + self, inputs: Optional[List[Port[AnalogLink]]] = None, output: Optional[Port[AnalogLink]] = None + ) -> "AnalogMuxer": + if inputs is not None: + for i, input_port in enumerate(inputs): + cast(Block, builder.get_enclosing_block()).connect(input_port, self.inputs.request(str(i))) + if output is not None: + cast(Block, builder.get_enclosing_block()).connect(output, self.out) + return self class AnalogDemuxer(Interface, GeneratorBlock): - """Wrapper around AnalogSwitch that provides demuxing functionality - multiple source ports, one sink port. - """ - def __init__(self) -> None: - super().__init__() - self.device = self.Block(AnalogSwitch()) - self.pwr = self.Export(self.device.pwr, [Power]) - self.gnd = self.Export(self.device.gnd, [Common]) - self.control = self.Export(self.device.control) - - self.outputs = self.Port(Vector(AnalogSource.empty())) - self.input = self.Export(self.device.com.adapt_to(AnalogSink( - voltage_limits=self.device.analog_voltage_limits, # this device only, voltages propagated - current_draw=self.outputs.hull(lambda x: x.link().current_drawn), - impedance=self.device.analog_on_resistance + self.outputs.hull(lambda x: x.link().sink_impedance) - ))) - - self.generator_param(self.outputs.requested()) - - @override - def generate(self) -> None: - super().generate() - self.outputs.defined() - for elt in self.get(self.outputs.requested()): - self.connect( - self.outputs.append_elt(AnalogSource.empty(), elt), - self.device.inputs.request(elt).adapt_to(AnalogSource( - voltage_out=self.input.link().voltage, - signal_out=self.input.link().signal, - current_limits=self.device.analog_current_limits, # this device only, voltages propagated - impedance=self.input.link().source_impedance + self.device.analog_on_resistance - ))) - - def demux_to(self, input: Optional[Port[AnalogLink]] = None, - outputs: Optional[List[Port[AnalogLink]]] = None) -> 'AnalogDemuxer': - if outputs is not None: - for i, output in enumerate(outputs): - cast(Block, builder.get_enclosing_block()).connect(output, self.outputs.request(str(i))) - if input is not None: - cast(Block, builder.get_enclosing_block()).connect(input, self.input) - return self + """Wrapper around AnalogSwitch that provides demuxing functionality - multiple source ports, one sink port.""" + + def __init__(self) -> None: + super().__init__() + self.device = self.Block(AnalogSwitch()) + self.pwr = self.Export(self.device.pwr, [Power]) + self.gnd = self.Export(self.device.gnd, [Common]) + self.control = self.Export(self.device.control) + + self.outputs = self.Port(Vector(AnalogSource.empty())) + self.input = self.Export( + self.device.com.adapt_to( + AnalogSink( + voltage_limits=self.device.analog_voltage_limits, # this device only, voltages propagated + current_draw=self.outputs.hull(lambda x: x.link().current_drawn), + impedance=self.device.analog_on_resistance + self.outputs.hull(lambda x: x.link().sink_impedance), + ) + ) + ) + + self.generator_param(self.outputs.requested()) + + @override + def generate(self) -> None: + super().generate() + self.outputs.defined() + for elt in self.get(self.outputs.requested()): + self.connect( + self.outputs.append_elt(AnalogSource.empty(), elt), + self.device.inputs.request(elt).adapt_to( + AnalogSource( + voltage_out=self.input.link().voltage, + signal_out=self.input.link().signal, + current_limits=self.device.analog_current_limits, # this device only, voltages propagated + impedance=self.input.link().source_impedance + self.device.analog_on_resistance, + ) + ), + ) + + def demux_to( + self, input: Optional[Port[AnalogLink]] = None, outputs: Optional[List[Port[AnalogLink]]] = None + ) -> "AnalogDemuxer": + if outputs is not None: + for i, output in enumerate(outputs): + cast(Block, builder.get_enclosing_block()).connect(output, self.outputs.request(str(i))) + if input is not None: + cast(Block, builder.get_enclosing_block()).connect(input, self.input) + return self diff --git a/edg/abstract_parts/AbstractAntenna.py b/edg/abstract_parts/AbstractAntenna.py index 60501f180..18e3c7bc6 100644 --- a/edg/abstract_parts/AbstractAntenna.py +++ b/edg/abstract_parts/AbstractAntenna.py @@ -8,44 +8,46 @@ @abstract_block class Antenna(Interface, Block): - def __init__(self, frequency: RangeLike, impedance: RangeLike = Range.all(), power: RangeLike = (0, 0)*Watt): - super().__init__() + def __init__(self, frequency: RangeLike, impedance: RangeLike = Range.all(), power: RangeLike = (0, 0) * Watt): + super().__init__() - self.frequency = self.ArgParameter(frequency) - self.actual_frequency_rating = self.Parameter(RangeExpr()) + self.frequency = self.ArgParameter(frequency) + self.actual_frequency_rating = self.Parameter(RangeExpr()) - self.impedance = self.ArgParameter(impedance) - self.actual_impedance = self.Parameter(RangeExpr()) + self.impedance = self.ArgParameter(impedance) + self.actual_impedance = self.Parameter(RangeExpr()) - self.power = self.ArgParameter(power) - self.actual_power_rating = self.Parameter(RangeExpr()) + self.power = self.ArgParameter(power) + self.actual_power_rating = self.Parameter(RangeExpr()) - self.a = self.Port(Passive.empty(), [Input]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.a = self.Port(Passive.empty(), [Input]) + self.gnd = self.Port(Ground.empty(), [Common]) @non_library class TableAntenna(Antenna, PartsTableSelector, GeneratorBlock): - REFDES_PREFIX = 'ANT' - - FREQUENCY_RATING = PartsTableColumn(Range) - IMPEDANCE = PartsTableColumn(Range) - POWER_RATING = PartsTableColumn(Range) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.frequency, self.power, self.impedance) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - self.get(self.frequency).fuzzy_in(row[self.FREQUENCY_RATING]) and \ - row[self.IMPEDANCE].fuzzy_in(self.get(self.impedance)) and\ - self.get(self.power).fuzzy_in(row[self.POWER_RATING]) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_frequency_rating, row[self.FREQUENCY_RATING]) - self.assign(self.actual_power_rating, row[self.POWER_RATING]) - self.assign(self.actual_impedance, row[self.IMPEDANCE]) + REFDES_PREFIX = "ANT" + + FREQUENCY_RATING = PartsTableColumn(Range) + IMPEDANCE = PartsTableColumn(Range) + POWER_RATING = PartsTableColumn(Range) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.frequency, self.power, self.impedance) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return ( + super()._row_filter(row) + and self.get(self.frequency).fuzzy_in(row[self.FREQUENCY_RATING]) + and row[self.IMPEDANCE].fuzzy_in(self.get(self.impedance)) + and self.get(self.power).fuzzy_in(row[self.POWER_RATING]) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_frequency_rating, row[self.FREQUENCY_RATING]) + self.assign(self.actual_power_rating, row[self.POWER_RATING]) + self.assign(self.actual_impedance, row[self.IMPEDANCE]) diff --git a/edg/abstract_parts/AbstractBjt.py b/edg/abstract_parts/AbstractBjt.py index 4e5db0df5..97bd7e26a 100644 --- a/edg/abstract_parts/AbstractBjt.py +++ b/edg/abstract_parts/AbstractBjt.py @@ -9,106 +9,131 @@ @abstract_block class Bjt(KiCadImportableBlock, DiscreteSemiconductor, HasStandardFootprint): - """Base class for untyped BJTs - """ - _STANDARD_FOOTPRINT = lambda: BjtStandardFootprint - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - # TODO actually check that the device channel corresponds with the schematic? - assert symbol_name.startswith('Device:Q_NPN_') or symbol_name.startswith('Device:Q_PNP_') - assert symbol_name.removeprefix('Device:Q_NPN_').removeprefix('Device:Q_PNP_') in \ - ('BCE', 'BEC', 'CBE', 'CEB', 'EBC', 'ECB') - return {'B': self.base, 'C': self.collector, 'E': self.emitter} - - @staticmethod - def Npn(*args: Any, **kwargs: Any) -> 'Bjt': - return Bjt(*args, **kwargs, channel='NPN') - - @staticmethod - def Pnp(*args: Any, **kwargs: Any) -> 'Bjt': - return Bjt(*args, **kwargs, channel='PNP') - - def __init__(self, collector_voltage: RangeLike, collector_current: RangeLike, *, - gain: RangeLike = Range.all(), power: RangeLike = Range.exact(0), - channel: StringLike = StringExpr()) -> None: - super().__init__() - - self.base = self.Port(Passive.empty()) - self.collector = self.Port(Passive.empty()) - self.emitter = self.Port(Passive.empty()) - - self.collector_voltage = self.ArgParameter(collector_voltage) - self.collector_current = self.ArgParameter(collector_current) - self.gain = self.ArgParameter(gain) - self.power = self.ArgParameter(power) - self.channel = self.ArgParameter(channel) - - self.actual_collector_voltage_rating = self.Parameter(RangeExpr()) - self.actual_collector_current_rating = self.Parameter(RangeExpr()) - self.actual_power_rating = self.Parameter(RangeExpr()) - self.actual_gain = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "Vce: ", DescriptionString.FormatUnits(self.actual_collector_voltage_rating, "V"), - " of operating: ", DescriptionString.FormatUnits(self.collector_voltage, "V"), "\n", - "Ice: ", DescriptionString.FormatUnits(self.actual_collector_current_rating, "A"), - " of operating: ", DescriptionString.FormatUnits(self.collector_current, "A"), "\n", - "Gain: ", DescriptionString.FormatUnits(self.actual_gain, ""), - " of spec: ", DescriptionString.FormatUnits(self.gain, ""), "\n", - "Pmax: ", DescriptionString.FormatUnits(self.actual_power_rating, "W"), - " of operating: ", DescriptionString.FormatUnits(self.power, "W") - ) + """Base class for untyped BJTs""" + + _STANDARD_FOOTPRINT = lambda: BjtStandardFootprint + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + # TODO actually check that the device channel corresponds with the schematic? + assert symbol_name.startswith("Device:Q_NPN_") or symbol_name.startswith("Device:Q_PNP_") + assert symbol_name.removeprefix("Device:Q_NPN_").removeprefix("Device:Q_PNP_") in ( + "BCE", + "BEC", + "CBE", + "CEB", + "EBC", + "ECB", + ) + return {"B": self.base, "C": self.collector, "E": self.emitter} + + @staticmethod + def Npn(*args: Any, **kwargs: Any) -> "Bjt": + return Bjt(*args, **kwargs, channel="NPN") + + @staticmethod + def Pnp(*args: Any, **kwargs: Any) -> "Bjt": + return Bjt(*args, **kwargs, channel="PNP") + + def __init__( + self, + collector_voltage: RangeLike, + collector_current: RangeLike, + *, + gain: RangeLike = Range.all(), + power: RangeLike = Range.exact(0), + channel: StringLike = StringExpr(), + ) -> None: + super().__init__() + + self.base = self.Port(Passive.empty()) + self.collector = self.Port(Passive.empty()) + self.emitter = self.Port(Passive.empty()) + + self.collector_voltage = self.ArgParameter(collector_voltage) + self.collector_current = self.ArgParameter(collector_current) + self.gain = self.ArgParameter(gain) + self.power = self.ArgParameter(power) + self.channel = self.ArgParameter(channel) + + self.actual_collector_voltage_rating = self.Parameter(RangeExpr()) + self.actual_collector_current_rating = self.Parameter(RangeExpr()) + self.actual_power_rating = self.Parameter(RangeExpr()) + self.actual_gain = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "Vce: ", + DescriptionString.FormatUnits(self.actual_collector_voltage_rating, "V"), + " of operating: ", + DescriptionString.FormatUnits(self.collector_voltage, "V"), + "\n", + "Ice: ", + DescriptionString.FormatUnits(self.actual_collector_current_rating, "A"), + " of operating: ", + DescriptionString.FormatUnits(self.collector_current, "A"), + "\n", + "Gain: ", + DescriptionString.FormatUnits(self.actual_gain, ""), + " of spec: ", + DescriptionString.FormatUnits(self.gain, ""), + "\n", + "Pmax: ", + DescriptionString.FormatUnits(self.actual_power_rating, "W"), + " of operating: ", + DescriptionString.FormatUnits(self.power, "W"), + ) class BjtStandardFootprint(StandardFootprint[Bjt]): - REFDES_PREFIX = 'Q' - - FOOTPRINT_PINNING_MAP = { - ( - 'Package_TO_SOT_SMD:SOT-23', - 'Package_TO_SOT_SMD:SOT-323_SC-70', - ): lambda block: { - '1': block.base, - '2': block.emitter, - '3': block.collector, - }, - 'Package_TO_SOT_SMD:SOT-89-3': lambda block: { - '1': block.base, - '2': block.collector, - '3': block.emitter, - }, - } + REFDES_PREFIX = "Q" + + FOOTPRINT_PINNING_MAP = { + ( + "Package_TO_SOT_SMD:SOT-23", + "Package_TO_SOT_SMD:SOT-323_SC-70", + ): lambda block: { + "1": block.base, + "2": block.emitter, + "3": block.collector, + }, + "Package_TO_SOT_SMD:SOT-89-3": lambda block: { + "1": block.base, + "2": block.collector, + "3": block.emitter, + }, + } class TableBjt(PartsTableSelector, Bjt): - VCE_RATING = PartsTableColumn(Range) - ICE_RATING = PartsTableColumn(Range) - GAIN = PartsTableColumn(Range) - POWER_RATING = PartsTableColumn(Range) - CHANNEL = PartsTableColumn(str) # either 'PNP' or 'NPN' - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.collector_voltage, self.collector_current, self.gain, self.power, self.channel) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - row[self.CHANNEL] == self.get(self.channel) and \ - self.get(self.collector_voltage).fuzzy_in(row[self.VCE_RATING]) and \ - self.get(self.collector_current).fuzzy_in(row[self.ICE_RATING]) and \ - row[self.GAIN].fuzzy_in(self.get(self.gain)) and \ - self.get(self.power).fuzzy_in(row[self.POWER_RATING]) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_collector_voltage_rating, row[self.VCE_RATING]) - self.assign(self.actual_collector_current_rating, row[self.ICE_RATING]) - self.assign(self.actual_gain, row[self.GAIN]) - self.assign(self.actual_power_rating, row[self.POWER_RATING]) + VCE_RATING = PartsTableColumn(Range) + ICE_RATING = PartsTableColumn(Range) + GAIN = PartsTableColumn(Range) + POWER_RATING = PartsTableColumn(Range) + CHANNEL = PartsTableColumn(str) # either 'PNP' or 'NPN' + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.collector_voltage, self.collector_current, self.gain, self.power, self.channel) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return ( + super()._row_filter(row) + and row[self.CHANNEL] == self.get(self.channel) + and self.get(self.collector_voltage).fuzzy_in(row[self.VCE_RATING]) + and self.get(self.collector_current).fuzzy_in(row[self.ICE_RATING]) + and row[self.GAIN].fuzzy_in(self.get(self.gain)) + and self.get(self.power).fuzzy_in(row[self.POWER_RATING]) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_collector_voltage_rating, row[self.VCE_RATING]) + self.assign(self.actual_collector_current_rating, row[self.ICE_RATING]) + self.assign(self.actual_gain, row[self.GAIN]) + self.assign(self.actual_power_rating, row[self.POWER_RATING]) diff --git a/edg/abstract_parts/AbstractCapacitor.py b/edg/abstract_parts/AbstractCapacitor.py index 16dee5d8e..84ca3352a 100644 --- a/edg/abstract_parts/AbstractCapacitor.py +++ b/edg/abstract_parts/AbstractCapacitor.py @@ -15,412 +15,464 @@ @abstract_block class UnpolarizedCapacitor(PassiveComponent): - """Base type for a capacitor, that defines its parameters and without ports (since capacitors can be polarized)""" - def __init__(self, capacitance: RangeLike, voltage: RangeLike, *, - voltage_rating_derating: FloatLike = 0.5, - exact_capacitance: BoolLike = False) -> None: - super().__init__() - - self.capacitance = self.ArgParameter(capacitance) - self.voltage = self.ArgParameter(voltage) # defined as operating voltage range - - # this is the scaling derating factor applied to the rated voltage spec - # eg, a value of 0.5 would mean the labeled rated voltage must be 2x the actual voltage - # 0.5 is the general rule of thumb for ceramic capacitors: https://www.sparkfun.com/news/1271 - # this does not apply to capacitance derating, which is handled separately - self.voltage_rating_derating = self.ArgParameter(voltage_rating_derating) - - # indicates whether the capacitance is exact (True) or nominal (False - typical case) - # in particular, nominal capacitance does not capacitance derate - self.exact_capacitance = self.ArgParameter(exact_capacitance) - - self.actual_capacitance = self.Parameter(RangeExpr()) - self.actual_voltage_rating = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "capacitance: ", DescriptionString.FormatUnits(self.actual_capacitance, "F"), - " of spec: ", DescriptionString.FormatUnits(self.capacitance, "F"), "\n", - "voltage rating: ", DescriptionString.FormatUnits(self.actual_voltage_rating, "V"), - " of operating: ", DescriptionString.FormatUnits(self.voltage, "V") - ) + """Base type for a capacitor, that defines its parameters and without ports (since capacitors can be polarized)""" + + def __init__( + self, + capacitance: RangeLike, + voltage: RangeLike, + *, + voltage_rating_derating: FloatLike = 0.5, + exact_capacitance: BoolLike = False, + ) -> None: + super().__init__() + + self.capacitance = self.ArgParameter(capacitance) + self.voltage = self.ArgParameter(voltage) # defined as operating voltage range + + # this is the scaling derating factor applied to the rated voltage spec + # eg, a value of 0.5 would mean the labeled rated voltage must be 2x the actual voltage + # 0.5 is the general rule of thumb for ceramic capacitors: https://www.sparkfun.com/news/1271 + # this does not apply to capacitance derating, which is handled separately + self.voltage_rating_derating = self.ArgParameter(voltage_rating_derating) + + # indicates whether the capacitance is exact (True) or nominal (False - typical case) + # in particular, nominal capacitance does not capacitance derate + self.exact_capacitance = self.ArgParameter(exact_capacitance) + + self.actual_capacitance = self.Parameter(RangeExpr()) + self.actual_voltage_rating = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "capacitance: ", + DescriptionString.FormatUnits(self.actual_capacitance, "F"), + " of spec: ", + DescriptionString.FormatUnits(self.capacitance, "F"), + "\n", + "voltage rating: ", + DescriptionString.FormatUnits(self.actual_voltage_rating, "V"), + " of operating: ", + DescriptionString.FormatUnits(self.voltage, "V"), + ) + @abstract_block class Capacitor(UnpolarizedCapacitor, KiCadInstantiableBlock, HasStandardFootprint): - """Polarized capacitor, which we assume will be the default""" - _STANDARD_FOOTPRINT = lambda: CapacitorStandardFootprint - - CAPACITOR_REGEX = re.compile("^" + f"([\d.{PartParserUtil.SI_PREFIXES}]+)\s*F?" + - "\s*" + "((?:\+-|\+/-|±)?\s*[\d.]+\s*%)?" + - "\s*" + f"([\d.{PartParserUtil.SI_PREFIXES}]+\s*V)" + "$") - CAPACITOR_DEFAULT_TOL = 0.20 # TODO this should be unified elsewhere - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name in ('Device:C', 'Device:C_Small', 'Device:C_Polarized', 'Device:C_Polarized_Small') - return {'1': self.pos, '2': self.neg} - - @classmethod - def parse_capacitor(cls, value: str) -> Tuple[Range, Range]: - match = cls.CAPACITOR_REGEX.match(value) - assert match is not None, f"could not parse capacitor from value '{value}'" - center = PartParserUtil.parse_value(match.group(1), '') - voltage = PartParserUtil.parse_value(match.group(3), 'V') - if match.group(2) is not None: - tol_str = match.group(2) - if not tol_str.startswith('±'): # format conversion to more strict parser - tol_str = '±' + tol_str - capacitance = PartParserUtil.parse_abs_tolerance(tol_str, center, 'F') - else: - capacitance = Range.from_tolerance(center, (-cls.CAPACITOR_DEFAULT_TOL, cls.CAPACITOR_DEFAULT_TOL)) - return (capacitance, Range.zero_to_upper(voltage)) - - @classmethod - @override - def block_from_symbol(cls, symbol_name: str, properties: Mapping[str, str]) -> 'Capacitor': - return Capacitor(*cls.parse_capacitor(properties['Value'])) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - self.pos = self.Port(Passive.empty()) - self.neg = self.Port(Passive.empty()) + """Polarized capacitor, which we assume will be the default""" + + _STANDARD_FOOTPRINT = lambda: CapacitorStandardFootprint + + CAPACITOR_REGEX = re.compile( + "^" + + f"([\d.{PartParserUtil.SI_PREFIXES}]+)\s*F?" + + "\s*" + + "((?:\+-|\+/-|±)?\s*[\d.]+\s*%)?" + + "\s*" + + f"([\d.{PartParserUtil.SI_PREFIXES}]+\s*V)" + + "$" + ) + CAPACITOR_DEFAULT_TOL = 0.20 # TODO this should be unified elsewhere + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name in ("Device:C", "Device:C_Small", "Device:C_Polarized", "Device:C_Polarized_Small") + return {"1": self.pos, "2": self.neg} + + @classmethod + def parse_capacitor(cls, value: str) -> Tuple[Range, Range]: + match = cls.CAPACITOR_REGEX.match(value) + assert match is not None, f"could not parse capacitor from value '{value}'" + center = PartParserUtil.parse_value(match.group(1), "") + voltage = PartParserUtil.parse_value(match.group(3), "V") + if match.group(2) is not None: + tol_str = match.group(2) + if not tol_str.startswith("±"): # format conversion to more strict parser + tol_str = "±" + tol_str + capacitance = PartParserUtil.parse_abs_tolerance(tol_str, center, "F") + else: + capacitance = Range.from_tolerance(center, (-cls.CAPACITOR_DEFAULT_TOL, cls.CAPACITOR_DEFAULT_TOL)) + return (capacitance, Range.zero_to_upper(voltage)) + + @classmethod + @override + def block_from_symbol(cls, symbol_name: str, properties: Mapping[str, str]) -> "Capacitor": + return Capacitor(*cls.parse_capacitor(properties["Value"])) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.pos = self.Port(Passive.empty()) + self.neg = self.Port(Passive.empty()) class CapacitorStandardFootprint(StandardFootprint[Capacitor]): - REFDES_PREFIX = 'C' - - # IMPORTANT! DummyFootprint doesn't use this, it will break on anything that isn't this pinning - FOOTPRINT_PINNING_MAP = { - ( - 'Capacitor_SMD:C_0201_0603Metric', - 'Capacitor_SMD:C_0402_1005Metric', - 'Capacitor_SMD:C_0603_1608Metric', - 'Capacitor_SMD:C_0805_2012Metric', - 'Capacitor_SMD:C_1206_3216Metric', - 'Capacitor_SMD:C_1210_3225Metric', - 'Capacitor_SMD:C_1812_4532Metric', - 'Capacitor_SMD:C_2512_6332Metric', - - 'Capacitor_SMD:CP_Elec_3x5.3', - 'Capacitor_SMD:CP_Elec_3x5.4', - 'Capacitor_SMD:CP_Elec_4x3', - 'Capacitor_SMD:CP_Elec_4x3.9', - 'Capacitor_SMD:CP_Elec_4x4.5', - 'Capacitor_SMD:CP_Elec_4x5.3', - 'Capacitor_SMD:CP_Elec_4x5.4', - 'Capacitor_SMD:CP_Elec_4x5.7', - 'Capacitor_SMD:CP_Elec_4x5.8', - 'Capacitor_SMD:CP_Elec_5x3', - 'Capacitor_SMD:CP_Elec_5x3.9', - 'Capacitor_SMD:CP_Elec_5x4.4', - 'Capacitor_SMD:CP_Elec_5x4.5', - 'Capacitor_SMD:CP_Elec_5x5.3', - 'Capacitor_SMD:CP_Elec_5x5.4', - 'Capacitor_SMD:CP_Elec_5x5.7', - 'Capacitor_SMD:CP_Elec_5x5.8', - 'Capacitor_SMD:CP_Elec_5x5.9', - 'Capacitor_SMD:CP_Elec_6.3x3', - 'Capacitor_SMD:CP_Elec_6.3x3.9', - 'Capacitor_SMD:CP_Elec_6.3x4.5', - 'Capacitor_SMD:CP_Elec_6.3x4.9', - 'Capacitor_SMD:CP_Elec_6.3x5.2', - 'Capacitor_SMD:CP_Elec_6.3x5.3', - 'Capacitor_SMD:CP_Elec_6.3x5.4', - 'Capacitor_SMD:CP_Elec_6.3x5.7', - 'Capacitor_SMD:CP_Elec_6.3x5.8', - 'Capacitor_SMD:CP_Elec_6.3x5.9', - 'Capacitor_SMD:CP_Elec_6.3x7.7', - 'Capacitor_SMD:CP_Elec_6.3x9.9', - 'Capacitor_SMD:CP_Elec_8x5.4', - 'Capacitor_SMD:CP_Elec_8x6.2', - 'Capacitor_SMD:CP_Elec_8x6.5', - 'Capacitor_SMD:CP_Elec_8x6.7', - 'Capacitor_SMD:CP_Elec_8x6.9', - 'Capacitor_SMD:CP_Elec_8x10', - 'Capacitor_SMD:CP_Elec_8x10.5', - 'Capacitor_SMD:CP_Elec_8x11.9', - 'Capacitor_SMD:CP_Elec_10x7.7', - 'Capacitor_SMD:CP_Elec_10x7.9', - 'Capacitor_SMD:CP_Elec_10x10', - 'Capacitor_SMD:CP_Elec_10x10.5', - 'Capacitor_SMD:CP_Elec_10x12.5', - 'Capacitor_SMD:CP_Elec_10x12.6', - 'Capacitor_SMD:CP_Elec_10x14.3', - 'Capacitor_SMD:CP_Elec_16x17.5', - 'Capacitor_SMD:CP_Elec_16x22', - 'Capacitor_SMD:CP_Elec_18x7.5', - 'Capacitor_SMD:CP_Elec_18x22', - ): lambda block: { - '1': block.pos, - '2': block.neg, - }, - } + REFDES_PREFIX = "C" + + # IMPORTANT! DummyFootprint doesn't use this, it will break on anything that isn't this pinning + FOOTPRINT_PINNING_MAP = { + ( + "Capacitor_SMD:C_0201_0603Metric", + "Capacitor_SMD:C_0402_1005Metric", + "Capacitor_SMD:C_0603_1608Metric", + "Capacitor_SMD:C_0805_2012Metric", + "Capacitor_SMD:C_1206_3216Metric", + "Capacitor_SMD:C_1210_3225Metric", + "Capacitor_SMD:C_1812_4532Metric", + "Capacitor_SMD:C_2512_6332Metric", + "Capacitor_SMD:CP_Elec_3x5.3", + "Capacitor_SMD:CP_Elec_3x5.4", + "Capacitor_SMD:CP_Elec_4x3", + "Capacitor_SMD:CP_Elec_4x3.9", + "Capacitor_SMD:CP_Elec_4x4.5", + "Capacitor_SMD:CP_Elec_4x5.3", + "Capacitor_SMD:CP_Elec_4x5.4", + "Capacitor_SMD:CP_Elec_4x5.7", + "Capacitor_SMD:CP_Elec_4x5.8", + "Capacitor_SMD:CP_Elec_5x3", + "Capacitor_SMD:CP_Elec_5x3.9", + "Capacitor_SMD:CP_Elec_5x4.4", + "Capacitor_SMD:CP_Elec_5x4.5", + "Capacitor_SMD:CP_Elec_5x5.3", + "Capacitor_SMD:CP_Elec_5x5.4", + "Capacitor_SMD:CP_Elec_5x5.7", + "Capacitor_SMD:CP_Elec_5x5.8", + "Capacitor_SMD:CP_Elec_5x5.9", + "Capacitor_SMD:CP_Elec_6.3x3", + "Capacitor_SMD:CP_Elec_6.3x3.9", + "Capacitor_SMD:CP_Elec_6.3x4.5", + "Capacitor_SMD:CP_Elec_6.3x4.9", + "Capacitor_SMD:CP_Elec_6.3x5.2", + "Capacitor_SMD:CP_Elec_6.3x5.3", + "Capacitor_SMD:CP_Elec_6.3x5.4", + "Capacitor_SMD:CP_Elec_6.3x5.7", + "Capacitor_SMD:CP_Elec_6.3x5.8", + "Capacitor_SMD:CP_Elec_6.3x5.9", + "Capacitor_SMD:CP_Elec_6.3x7.7", + "Capacitor_SMD:CP_Elec_6.3x9.9", + "Capacitor_SMD:CP_Elec_8x5.4", + "Capacitor_SMD:CP_Elec_8x6.2", + "Capacitor_SMD:CP_Elec_8x6.5", + "Capacitor_SMD:CP_Elec_8x6.7", + "Capacitor_SMD:CP_Elec_8x6.9", + "Capacitor_SMD:CP_Elec_8x10", + "Capacitor_SMD:CP_Elec_8x10.5", + "Capacitor_SMD:CP_Elec_8x11.9", + "Capacitor_SMD:CP_Elec_10x7.7", + "Capacitor_SMD:CP_Elec_10x7.9", + "Capacitor_SMD:CP_Elec_10x10", + "Capacitor_SMD:CP_Elec_10x10.5", + "Capacitor_SMD:CP_Elec_10x12.5", + "Capacitor_SMD:CP_Elec_10x12.6", + "Capacitor_SMD:CP_Elec_10x14.3", + "Capacitor_SMD:CP_Elec_16x17.5", + "Capacitor_SMD:CP_Elec_16x22", + "Capacitor_SMD:CP_Elec_18x7.5", + "Capacitor_SMD:CP_Elec_18x22", + ): lambda block: { + "1": block.pos, + "2": block.neg, + }, + } @abstract_block class CeramicCapacitor(Capacitor): - """Abstract base class for ceramic capacitors, which appear more ideal in terms of lower ESP""" - pass + """Abstract base class for ceramic capacitors, which appear more ideal in terms of lower ESP""" + + pass @abstract_block class AluminumCapacitor(Capacitor): - """Abstract base class for aluminum electrolytic capacitors capacitors which provide compact bulk capacitance - but at the cost of ESR""" - pass + """Abstract base class for aluminum electrolytic capacitors capacitors which provide compact bulk capacitance + but at the cost of ESR""" + + pass @non_library class TableCapacitor(PartsTableSelector, Capacitor): - """Abstract table-based capacitor, providing some interface column definitions.""" - CAPACITANCE = PartsTableColumn(Range) - NOMINAL_CAPACITANCE = PartsTableColumn(float) # nominal capacitance, even with asymmetrical tolerances - VOLTAGE_RATING = PartsTableColumn(Range) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.capacitance, self.voltage, self.voltage_rating_derating, self.exact_capacitance) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) - self.assign(self.actual_capacitance, row[self.CAPACITANCE]) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - derated_voltage = self.get(self.voltage) / self.get(self.voltage_rating_derating) - return super()._row_filter(row) and \ - derated_voltage.fuzzy_in(row[self.VOLTAGE_RATING]) and \ - self._row_filter_capacitance(row) - - def _row_filter_capacitance(self, row: PartsTableRow) -> bool: - return row[self.CAPACITANCE].fuzzy_in(self.get(self.capacitance)) - - @classmethod - @override - def _row_sort_by(cls, row: PartsTableRow) -> Any: - return (ESeriesUtil.series_of(row[cls.NOMINAL_CAPACITANCE], default=ESeriesUtil.SERIES_MAX + 1), - super()._row_sort_by(row)) + """Abstract table-based capacitor, providing some interface column definitions.""" + + CAPACITANCE = PartsTableColumn(Range) + NOMINAL_CAPACITANCE = PartsTableColumn(float) # nominal capacitance, even with asymmetrical tolerances + VOLTAGE_RATING = PartsTableColumn(Range) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.capacitance, self.voltage, self.voltage_rating_derating, self.exact_capacitance) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) + self.assign(self.actual_capacitance, row[self.CAPACITANCE]) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + derated_voltage = self.get(self.voltage) / self.get(self.voltage_rating_derating) + return ( + super()._row_filter(row) + and derated_voltage.fuzzy_in(row[self.VOLTAGE_RATING]) + and self._row_filter_capacitance(row) + ) + + def _row_filter_capacitance(self, row: PartsTableRow) -> bool: + return row[self.CAPACITANCE].fuzzy_in(self.get(self.capacitance)) + + @classmethod + @override + def _row_sort_by(cls, row: PartsTableRow) -> Any: + return ( + ESeriesUtil.series_of(row[cls.NOMINAL_CAPACITANCE], default=ESeriesUtil.SERIES_MAX + 1), + super()._row_sort_by(row), + ) + @non_library class TableDeratingCapacitor(TableCapacitor): - """Abstract table-based capacitor with derating based on a part-part voltage coefficient.""" - VOLTCO = PartsTableColumn(float) - DERATED_CAPACITANCE = PartsTableColumn(Range) - - PARALLEL_COUNT = PartsTableColumn(int) - PARALLEL_CAPACITANCE = PartsTableColumn(Range) - PARALLEL_DERATED_CAPACITANCE = PartsTableColumn(Range) - - MAX_PARALLEL_COUNT = 10 # maximum parallel capacitors that can be generated - - # default derating parameters - DERATE_MIN_VOLTAGE = 3.6 # voltage at which derating is zero - DERATE_MIN_CAPACITANCE = 1.0e-6 - DERATE_LOWEST = 0.2 # floor for maximum derating factor - # LOOSELY approximated from https://www.maximintegrated.com/en/design/technical-documents/tutorials/5/5527.html - - def __init__(self, *args: Any, single_nominal_capacitance: RangeLike = (0, 22)*uFarad, **kwargs: Any): - super().__init__(*args, **kwargs) - self.single_nominal_capacitance = self.ArgParameter(single_nominal_capacitance) - self.generator_param(self.single_nominal_capacitance) - - self.actual_derated_capacitance = self.Parameter(RangeExpr()) - - @override - def _row_filter_capacitance(self, row: PartsTableRow) -> bool: - # post-derating capacitance filtering is in _table_postprocess - return Range.exact(row[self.NOMINAL_CAPACITANCE]).fuzzy_in(self.get(self.single_nominal_capacitance)) - - @override - def _table_postprocess(self, table: PartsTable) -> PartsTable: - def add_derated_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if not self.get(self.exact_capacitance): - derated = row[self.CAPACITANCE] - elif self.get(self.voltage).upper < self.DERATE_MIN_VOLTAGE: # zero derating at low voltages - derated = row[self.CAPACITANCE] - elif row[self.NOMINAL_CAPACITANCE] <= self.DERATE_MIN_CAPACITANCE: # don't derate below 1uF - derated = row[self.CAPACITANCE] - else: # actually derate - factor = 1 - row[self.VOLTCO] * (self.get(self.voltage).upper - 3.6) - if factor < self.DERATE_LOWEST: - factor = self.DERATE_LOWEST - derated = row[self.CAPACITANCE] * Range(factor, 1) - - if derated.lower == 0: # in case where tolerance is the nominal value - return None - # ceil is needed so that it generates the maximum number of capacitors, especially for where the upper bound - # is infinite (like decoupling capacitors for power converters), but float issues can cause the rounding to be - # too aggressive even for an exact match, so this reduces the count before ceil-ing it - count = math.ceil(self.get(self.capacitance).lower / derated.lower * (1-Range.DOUBLE_FLOAT_ROUND_FACTOR)) - derated_parallel_capacitance = derated * count - if not derated_parallel_capacitance.fuzzy_in(self.get(self.capacitance)): # not satisfying spec, remove row - return None - - if count > self.MAX_PARALLEL_COUNT: - return None - - return {self.DERATED_CAPACITANCE: derated, - self.PARALLEL_COUNT: count, - self.PARALLEL_DERATED_CAPACITANCE: derated_parallel_capacitance, - self.PARALLEL_CAPACITANCE: row[self.CAPACITANCE] * count} - - return table.map_new_columns(add_derated_row).filter(lambda row: ( - row[self.PARALLEL_DERATED_CAPACITANCE].fuzzy_in(self.get(self.capacitance)) - )) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - """This one is weird. Because this is the last in the class order, this is called last. - So the top subclass needs explicit logic to handle parallel capacitors.""" - super()._row_generate(row) - if row[self.PARALLEL_COUNT] == 1: - self.assign(self.actual_derated_capacitance, row[self.DERATED_CAPACITANCE]) - else: - self.assign(self.actual_part, f"{row[self.PARALLEL_COUNT]}x {row[self.PART_NUMBER_COL]}") - self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) - self.assign(self.actual_capacitance, row[self.PARALLEL_CAPACITANCE]) - self.assign(self.actual_derated_capacitance, row[self.PARALLEL_DERATED_CAPACITANCE]) - - @abstractmethod - def _make_parallel_footprints(self, row: PartsTableRow) -> None: - """Given a selected part (row), creates the parallel internal capacitors. Implement me.""" - ... + """Abstract table-based capacitor with derating based on a part-part voltage coefficient.""" + + VOLTCO = PartsTableColumn(float) + DERATED_CAPACITANCE = PartsTableColumn(Range) + + PARALLEL_COUNT = PartsTableColumn(int) + PARALLEL_CAPACITANCE = PartsTableColumn(Range) + PARALLEL_DERATED_CAPACITANCE = PartsTableColumn(Range) + + MAX_PARALLEL_COUNT = 10 # maximum parallel capacitors that can be generated + + # default derating parameters + DERATE_MIN_VOLTAGE = 3.6 # voltage at which derating is zero + DERATE_MIN_CAPACITANCE = 1.0e-6 + DERATE_LOWEST = 0.2 # floor for maximum derating factor + # LOOSELY approximated from https://www.maximintegrated.com/en/design/technical-documents/tutorials/5/5527.html + + def __init__(self, *args: Any, single_nominal_capacitance: RangeLike = (0, 22) * uFarad, **kwargs: Any): + super().__init__(*args, **kwargs) + self.single_nominal_capacitance = self.ArgParameter(single_nominal_capacitance) + self.generator_param(self.single_nominal_capacitance) + + self.actual_derated_capacitance = self.Parameter(RangeExpr()) + + @override + def _row_filter_capacitance(self, row: PartsTableRow) -> bool: + # post-derating capacitance filtering is in _table_postprocess + return Range.exact(row[self.NOMINAL_CAPACITANCE]).fuzzy_in(self.get(self.single_nominal_capacitance)) + + @override + def _table_postprocess(self, table: PartsTable) -> PartsTable: + def add_derated_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if not self.get(self.exact_capacitance): + derated = row[self.CAPACITANCE] + elif self.get(self.voltage).upper < self.DERATE_MIN_VOLTAGE: # zero derating at low voltages + derated = row[self.CAPACITANCE] + elif row[self.NOMINAL_CAPACITANCE] <= self.DERATE_MIN_CAPACITANCE: # don't derate below 1uF + derated = row[self.CAPACITANCE] + else: # actually derate + factor = 1 - row[self.VOLTCO] * (self.get(self.voltage).upper - 3.6) + if factor < self.DERATE_LOWEST: + factor = self.DERATE_LOWEST + derated = row[self.CAPACITANCE] * Range(factor, 1) + + if derated.lower == 0: # in case where tolerance is the nominal value + return None + # ceil is needed so that it generates the maximum number of capacitors, especially for where the upper bound + # is infinite (like decoupling capacitors for power converters), but float issues can cause the rounding to be + # too aggressive even for an exact match, so this reduces the count before ceil-ing it + count = math.ceil(self.get(self.capacitance).lower / derated.lower * (1 - Range.DOUBLE_FLOAT_ROUND_FACTOR)) + derated_parallel_capacitance = derated * count + if not derated_parallel_capacitance.fuzzy_in(self.get(self.capacitance)): # not satisfying spec, remove row + return None + + if count > self.MAX_PARALLEL_COUNT: + return None + + return { + self.DERATED_CAPACITANCE: derated, + self.PARALLEL_COUNT: count, + self.PARALLEL_DERATED_CAPACITANCE: derated_parallel_capacitance, + self.PARALLEL_CAPACITANCE: row[self.CAPACITANCE] * count, + } + + return table.map_new_columns(add_derated_row).filter( + lambda row: (row[self.PARALLEL_DERATED_CAPACITANCE].fuzzy_in(self.get(self.capacitance))) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + """This one is weird. Because this is the last in the class order, this is called last. + So the top subclass needs explicit logic to handle parallel capacitors.""" + super()._row_generate(row) + if row[self.PARALLEL_COUNT] == 1: + self.assign(self.actual_derated_capacitance, row[self.DERATED_CAPACITANCE]) + else: + self.assign(self.actual_part, f"{row[self.PARALLEL_COUNT]}x {row[self.PART_NUMBER_COL]}") + self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) + self.assign(self.actual_capacitance, row[self.PARALLEL_CAPACITANCE]) + self.assign(self.actual_derated_capacitance, row[self.PARALLEL_DERATED_CAPACITANCE]) + + @abstractmethod + def _make_parallel_footprints(self, row: PartsTableRow) -> None: + """Given a selected part (row), creates the parallel internal capacitors. Implement me.""" + ... class DummyCapacitorFootprint(DummyDevice, Capacitor, FootprintBlock): - """Dummy capacitor that takes in all its parameters (footprint, value, etc) and does not do any computation. - Used as the leaf block for generating parallel capacitors. - - TODO: use footprint table? - """ - def __init__(self, footprint: StringLike = "", manufacturer: StringLike = "", part_number: StringLike = "", - value: StringLike = "", - *args: Any, **kwargs: Any): - super().__init__(*args, **kwargs) - - self.footprint( - 'C', footprint, - { - '1': self.pos, - '2': self.neg, - }, - mfr=manufacturer, part=part_number, - value=value - ) + """Dummy capacitor that takes in all its parameters (footprint, value, etc) and does not do any computation. + Used as the leaf block for generating parallel capacitors. + + TODO: use footprint table? + """ + + def __init__( + self, + footprint: StringLike = "", + manufacturer: StringLike = "", + part_number: StringLike = "", + value: StringLike = "", + *args: Any, + **kwargs: Any, + ): + super().__init__(*args, **kwargs) + + self.footprint( + "C", + footprint, + { + "1": self.pos, + "2": self.neg, + }, + mfr=manufacturer, + part=part_number, + value=value, + ) class DecouplingCapacitor(DiscreteApplication, KiCadImportableBlock): - """Optionally polarized capacitor used for DC decoupling, with VoltageSink connections with voltage inference. - Implemented as a shim block.""" - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name in ('Device:C', 'Device:C_Small', 'Device:C_Polarized', 'Device:C_Polarized_Small') - return {'1': self.pwr, '2': self.gnd} - - def __init__(self, capacitance: RangeLike, *, exact_capacitance: BoolLike = False) -> None: - super().__init__() - - self.cap = self.Block(Capacitor(capacitance, voltage=RangeExpr(), exact_capacitance=exact_capacitance)) - self.gnd = self.Export(self.cap.neg.adapt_to(Ground()), [Common]) - self.pwr = self.Export(self.cap.pos.adapt_to(VoltageSink.from_gnd( - self.gnd, - voltage_limits=self.cap.actual_voltage_rating, - current_draw=0*Amp(tol=0) - )), [Power, InOut]) - - self.assign(self.cap.voltage, self.pwr.link().voltage - self.gnd.link().voltage) - - # TODO there should be a way to forward the description string of the inner element - - def connected(self, gnd: Optional[Port[GroundLink]] = None, pwr: Optional[Port[VoltageLink]] = None) -> \ - 'DecouplingCapacitor': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if gnd is not None: - cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) - if pwr is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr, self.pwr) - return self + """Optionally polarized capacitor used for DC decoupling, with VoltageSink connections with voltage inference. + Implemented as a shim block.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name in ("Device:C", "Device:C_Small", "Device:C_Polarized", "Device:C_Polarized_Small") + return {"1": self.pwr, "2": self.gnd} + + def __init__(self, capacitance: RangeLike, *, exact_capacitance: BoolLike = False) -> None: + super().__init__() + + self.cap = self.Block(Capacitor(capacitance, voltage=RangeExpr(), exact_capacitance=exact_capacitance)) + self.gnd = self.Export(self.cap.neg.adapt_to(Ground()), [Common]) + self.pwr = self.Export( + self.cap.pos.adapt_to( + VoltageSink.from_gnd( + self.gnd, voltage_limits=self.cap.actual_voltage_rating, current_draw=0 * Amp(tol=0) + ) + ), + [Power, InOut], + ) + + self.assign(self.cap.voltage, self.pwr.link().voltage - self.gnd.link().voltage) + + # TODO there should be a way to forward the description string of the inner element + + def connected( + self, gnd: Optional[Port[GroundLink]] = None, pwr: Optional[Port[VoltageLink]] = None + ) -> "DecouplingCapacitor": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if gnd is not None: + cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) + if pwr is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr, self.pwr) + return self class AnalogCapacitor(DiscreteApplication, KiCadImportableBlock): - """Capacitor attached to an analog line, that presents as an open model-wise. - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name in ('Device:C', 'Device:C_Small', 'Device:C_Polarized', 'Device:C_Polarized_Small') - return {'1': self.io, '2': self.gnd} + """Capacitor attached to an analog line, that presents as an open model-wise.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name in ("Device:C", "Device:C_Small", "Device:C_Polarized", "Device:C_Polarized_Small") + return {"1": self.io, "2": self.gnd} - def __init__(self, capacitance: RangeLike, *, exact_capacitance: BoolLike = False) -> None: - super().__init__() + def __init__(self, capacitance: RangeLike, *, exact_capacitance: BoolLike = False) -> None: + super().__init__() - self.cap = self.Block(Capacitor(capacitance, voltage=RangeExpr(), exact_capacitance=exact_capacitance)) - self.gnd = self.Export(self.cap.neg.adapt_to(Ground()), [Common]) - self.io = self.Export(self.cap.pos.adapt_to(AnalogSink()), [InOut]) # ideal open port + self.cap = self.Block(Capacitor(capacitance, voltage=RangeExpr(), exact_capacitance=exact_capacitance)) + self.gnd = self.Export(self.cap.neg.adapt_to(Ground()), [Common]) + self.io = self.Export(self.cap.pos.adapt_to(AnalogSink()), [InOut]) # ideal open port - self.assign(self.cap.voltage, self.io.link().voltage - self.gnd.link().voltage) + self.assign(self.cap.voltage, self.io.link().voltage - self.gnd.link().voltage) - def connected(self, gnd: Optional[Port[GroundLink]] = None, io: Optional[Port[AnalogLink]] = None) -> \ - 'AnalogCapacitor': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if gnd is not None: - cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) - if io is not None: - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + def connected( + self, gnd: Optional[Port[GroundLink]] = None, io: Optional[Port[AnalogLink]] = None + ) -> "AnalogCapacitor": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if gnd is not None: + cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) + if io is not None: + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class CombinedCapacitorElement(Capacitor): # to avoid an abstract part error - @override - def contents(self) -> None: - super().contents() - self.assign(self.actual_capacitance, self.capacitance) # fake it, since a combined capacitance is handwavey + @override + def contents(self) -> None: + super().contents() + self.assign(self.actual_capacitance, self.capacitance) # fake it, since a combined capacitance is handwavey class CombinedCapacitor(MultipackDevice, MultipackBlock, GeneratorBlock): - """A packed capacitor that combines multiple individual capacitors into a single component, - with the sum of or taking the max of the constituent capacitances.""" - def __init__(self, *, extend_upper: BoolLike = False) -> None: - super().__init__() - - self.elements = self.PackedPart(PackedBlockArray(CombinedCapacitorElement(capacitance=RangeExpr(), - voltage=RangeExpr()))) - self.pos = self.PackedExport(self.elements.ports_array(lambda x: x.pos)) - self.neg = self.PackedExport(self.elements.ports_array(lambda x: x.neg)) - self.capacitances = self.PackedParameter(self.elements.params_array(lambda x: x.capacitance)) - self.voltages = self.PackedParameter(self.elements.params_array(lambda x: x.voltage)) - self.voltage_rating_deratings = self.PackedParameter(self.elements.params_array(lambda x: x.voltage_rating_derating)) - self.exact_capacitances = self.PackedParameter(self.elements.params_array(lambda x: x.exact_capacitance)) - - self.actual_capacitance = self.Parameter(RangeExpr()) - self.actual_voltage_rating = self.Parameter(RangeExpr()) - self.unpacked_assign(self.elements.params(lambda x: x.actual_voltage_rating), self.actual_voltage_rating) - - self.extend_upper = self.ArgParameter(extend_upper) - self.generator_param(self.pos.requested(), self.neg.requested(), self.extend_upper) - - - @override - def generate(self) -> None: - super().generate() - capacitance = self.capacitances.sum() - if self.get(self.extend_upper): - capacitance = RangeExpr._to_expr_type((capacitance.lower(), float('inf'))) - self.cap = self.Block(Capacitor(capacitance, voltage=self.voltages.hull(), - exact_capacitance=self.exact_capacitances.all(), - voltage_rating_derating=self.voltage_rating_deratings.min())) - self.assign(self.actual_voltage_rating, self.cap.actual_voltage_rating) - self.assign(self.actual_capacitance, self.cap.actual_capacitance) - self.require(self.exact_capacitances.all_equal()) - - self.pos_merge = self.Block(PackedPassive()) - self.neg_merge = self.Block(PackedPassive()) - self.connect(self.cap.pos, self.pos_merge.merged) - self.connect(self.cap.neg, self.neg_merge.merged) - self.connect(self.pos, self.pos_merge.elts) - self.connect(self.neg, self.neg_merge.elts) + """A packed capacitor that combines multiple individual capacitors into a single component, + with the sum of or taking the max of the constituent capacitances.""" + + def __init__(self, *, extend_upper: BoolLike = False) -> None: + super().__init__() + + self.elements = self.PackedPart( + PackedBlockArray(CombinedCapacitorElement(capacitance=RangeExpr(), voltage=RangeExpr())) + ) + self.pos = self.PackedExport(self.elements.ports_array(lambda x: x.pos)) + self.neg = self.PackedExport(self.elements.ports_array(lambda x: x.neg)) + self.capacitances = self.PackedParameter(self.elements.params_array(lambda x: x.capacitance)) + self.voltages = self.PackedParameter(self.elements.params_array(lambda x: x.voltage)) + self.voltage_rating_deratings = self.PackedParameter( + self.elements.params_array(lambda x: x.voltage_rating_derating) + ) + self.exact_capacitances = self.PackedParameter(self.elements.params_array(lambda x: x.exact_capacitance)) + + self.actual_capacitance = self.Parameter(RangeExpr()) + self.actual_voltage_rating = self.Parameter(RangeExpr()) + self.unpacked_assign(self.elements.params(lambda x: x.actual_voltage_rating), self.actual_voltage_rating) + + self.extend_upper = self.ArgParameter(extend_upper) + self.generator_param(self.pos.requested(), self.neg.requested(), self.extend_upper) + + @override + def generate(self) -> None: + super().generate() + capacitance = self.capacitances.sum() + if self.get(self.extend_upper): + capacitance = RangeExpr._to_expr_type((capacitance.lower(), float("inf"))) + self.cap = self.Block( + Capacitor( + capacitance, + voltage=self.voltages.hull(), + exact_capacitance=self.exact_capacitances.all(), + voltage_rating_derating=self.voltage_rating_deratings.min(), + ) + ) + self.assign(self.actual_voltage_rating, self.cap.actual_voltage_rating) + self.assign(self.actual_capacitance, self.cap.actual_capacitance) + self.require(self.exact_capacitances.all_equal()) + + self.pos_merge = self.Block(PackedPassive()) + self.neg_merge = self.Block(PackedPassive()) + self.connect(self.cap.pos, self.pos_merge.merged) + self.connect(self.cap.neg, self.neg_merge.merged) + self.connect(self.pos, self.pos_merge.elts) + self.connect(self.neg, self.neg_merge.elts) diff --git a/edg/abstract_parts/AbstractComparator.py b/edg/abstract_parts/AbstractComparator.py index d03ab4c64..b898bdd0b 100644 --- a/edg/abstract_parts/AbstractComparator.py +++ b/edg/abstract_parts/AbstractComparator.py @@ -9,14 +9,15 @@ class Comparator(KiCadInstantiableBlock, Analog): """Abstract comparator interface, output goes high when inp > inn.""" + @override def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name in ('Simulation_SPICE:OPAMP', 'edg_importable:Opamp') - return {'+': self.inp, '-': self.inn, '3': self.out, 'V+': self.pwr, 'V-': self.gnd} + assert symbol_name in ("Simulation_SPICE:OPAMP", "edg_importable:Opamp") + return {"+": self.inp, "-": self.inn, "3": self.out, "V+": self.pwr, "V-": self.gnd} @classmethod @override - def block_from_symbol(cls, symbol_name: str, properties: Mapping[str, str]) -> 'Comparator': + def block_from_symbol(cls, symbol_name: str, properties: Mapping[str, str]) -> "Comparator": return Comparator() def __init__(self) -> None: @@ -40,9 +41,15 @@ class VoltageComparator(GeneratorBlock): TODO: maybe a version that takes an input analog signal? """ - def __init__(self, trip_voltage: RangeLike, *, invert: BoolLike = False, - input_impedance: RangeLike=(4.7, 47)*kOhm, - trip_ref: RangeLike=1.65*Volt(tol=0.10)): + + def __init__( + self, + trip_voltage: RangeLike, + *, + invert: BoolLike = False, + input_impedance: RangeLike = (4.7, 47) * kOhm, + trip_ref: RangeLike = 1.65 * Volt(tol=0.10), + ): super().__init__() self.comp = self.Block(Comparator()) self.gnd = self.Export(self.comp.gnd, [Common]) @@ -67,20 +74,22 @@ def generate(self) -> None: ref_pin: Port[AnalogLink] = self.ref ref_voltage = self.ref.link().signal else: - self.ref_div = self.Block(VoltageDivider( - output_voltage=self.trip_ref, - impedance=self.input_impedance, - )) + self.ref_div = self.Block( + VoltageDivider( + output_voltage=self.trip_ref, + impedance=self.input_impedance, + ) + ) self.connect(self.ref_div.input, self.pwr) self.connect(self.ref_div.gnd, self.gnd) ref_pin = self.ref_div.output ref_voltage = self.ref_div.output.link().signal - self.comp_div = self.Block(FeedbackVoltageDivider( - impedance=self.input_impedance, - output_voltage=ref_voltage, - assumed_input_voltage=self.trip_voltage - )) + self.comp_div = self.Block( + FeedbackVoltageDivider( + impedance=self.input_impedance, output_voltage=ref_voltage, assumed_input_voltage=self.trip_voltage + ) + ) self.assign(self.actual_trip_voltage, self.comp_div.actual_input_voltage) self.connect(self.comp_div.input, self.input.as_voltage_source()) self.connect(self.comp_div.gnd, self.gnd) diff --git a/edg/abstract_parts/AbstractConnector.py b/edg/abstract_parts/AbstractConnector.py index ae5b9e79f..1ab72111c 100644 --- a/edg/abstract_parts/AbstractConnector.py +++ b/edg/abstract_parts/AbstractConnector.py @@ -7,60 +7,64 @@ @abstract_block class BananaJack(Connector): - """Base class for a single terminal 4mm banana jack, such as used on test equipment.""" - def __init__(self) -> None: - super().__init__() - self.port = self.Port(Passive.empty()) + """Base class for a single terminal 4mm banana jack, such as used on test equipment.""" + + def __init__(self) -> None: + super().__init__() + self.port = self.Port(Passive.empty()) @abstract_block class BananaSafetyJack(BananaJack): - """Base class for a single terminal 4mm banana jack supporting a safety sheath, - such as on multimeter leads.""" + """Base class for a single terminal 4mm banana jack supporting a safety sheath, + such as on multimeter leads.""" @abstract_block class RfConnector(Connector): - """Base class for a RF connector, with a signal and ground. Signal is passive-typed.""" - def __init__(self) -> None: - super().__init__() - self.sig = self.Port(Passive.empty(), [Input]) - self.gnd = self.Port(Ground(), [Common]) + """Base class for a RF connector, with a signal and ground. Signal is passive-typed.""" + + def __init__(self) -> None: + super().__init__() + self.sig = self.Port(Passive.empty(), [Input]) + self.gnd = self.Port(Ground(), [Common]) class RfConnectorTestPoint(BlockInterfaceMixin[RfConnector]): - """Test point mixin that allows the footprint to take a name""" - def __init__(self, name: StringLike): - super().__init__() - self.tp_name = self.ArgParameter(name) + """Test point mixin that allows the footprint to take a name""" + + def __init__(self, name: StringLike): + super().__init__() + self.tp_name = self.ArgParameter(name) class RfConnectorAntenna(Antenna): - """RF connector used as an antenna""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.conn = self.Block(RfConnector()) - self.connect(self.conn.sig, self.a) - self.connect(self.conn.gnd, self.gnd) + """RF connector used as an antenna""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.conn = self.Block(RfConnector()) + self.connect(self.conn.sig, self.a) + self.connect(self.conn.gnd, self.gnd) @abstract_block class UflConnector(RfConnector): - """Base class for a U.FL / IPEX / UMCC connector, miniature RF connector.""" + """Base class for a U.FL / IPEX / UMCC connector, miniature RF connector.""" @abstract_block class SmaConnector(RfConnector): - """Base class for a SMA coax connector.""" + """Base class for a SMA coax connector.""" @abstract_block class SmaMConnector(SmaConnector): - """Base class for a SMA M connector, pin with internal threads. - Typically used on the antenna itself.""" + """Base class for a SMA M connector, pin with internal threads. + Typically used on the antenna itself.""" @abstract_block class SmaFConnector(SmaConnector): - """Base class for a SMA F connector, socket with external threads. - Typically used for an antenna connector for sub-2.4GHz applications; 2.4GHz uses RP-SMA.""" + """Base class for a SMA F connector, socket with external threads. + Typically used for an antenna connector for sub-2.4GHz applications; 2.4GHz uses RP-SMA.""" diff --git a/edg/abstract_parts/AbstractCrystal.py b/edg/abstract_parts/AbstractCrystal.py index 9a667f682..ee5c65152 100644 --- a/edg/abstract_parts/AbstractCrystal.py +++ b/edg/abstract_parts/AbstractCrystal.py @@ -10,123 +10,128 @@ @abstract_block class Crystal(DiscreteComponent, HasStandardFootprint): - _STANDARD_FOOTPRINT = lambda: CrystalStandardFootprint - - def __init__(self, frequency: RangeLike) -> None: - """Discrete crystal component.""" - super().__init__() - - self.frequency = self.ArgParameter(frequency) - self.actual_frequency = self.Parameter(RangeExpr()) - self.actual_capacitance = self.Parameter(FloatExpr()) - - self.crystal = self.Port(CrystalPort(self.actual_frequency), [InOut]) # set by subclass - self.gnd = self.Port(Ground(), [Common]) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "frequency: ", DescriptionString.FormatUnits(self.actual_frequency, "Hz"), - " of spec: ", DescriptionString.FormatUnits(self.frequency, "Hz"), "\n", - "capacitance: ", DescriptionString.FormatUnits(self.actual_capacitance, "F") - ) - - -class CrystalStandardFootprint(StandardFootprint['Crystal']): - REFDES_PREFIX = 'X' - - FOOTPRINT_PINNING_MAP = { - 'Oscillator:Oscillator_SMD_Abracon_ASE-4Pin_3.2x2.5mm': lambda block: { - '1': block.crystal.xtal_in, - '2': block.gnd, - '3': block.crystal.xtal_out, - '4': block.gnd, - }, - 'Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm': lambda block: { - '1': block.crystal.xtal_in, - '2': block.gnd, - '3': block.crystal.xtal_out, - '4': block.gnd, - }, - 'Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm': lambda block: { - '1': block.crystal.xtal_in, - '2': block.gnd, - '3': block.crystal.xtal_out, - '4': block.gnd, - }, - } + _STANDARD_FOOTPRINT = lambda: CrystalStandardFootprint + + def __init__(self, frequency: RangeLike) -> None: + """Discrete crystal component.""" + super().__init__() + + self.frequency = self.ArgParameter(frequency) + self.actual_frequency = self.Parameter(RangeExpr()) + self.actual_capacitance = self.Parameter(FloatExpr()) + + self.crystal = self.Port(CrystalPort(self.actual_frequency), [InOut]) # set by subclass + self.gnd = self.Port(Ground(), [Common]) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "frequency: ", + DescriptionString.FormatUnits(self.actual_frequency, "Hz"), + " of spec: ", + DescriptionString.FormatUnits(self.frequency, "Hz"), + "\n", + "capacitance: ", + DescriptionString.FormatUnits(self.actual_capacitance, "F"), + ) + + +class CrystalStandardFootprint(StandardFootprint["Crystal"]): + REFDES_PREFIX = "X" + + FOOTPRINT_PINNING_MAP = { + "Oscillator:Oscillator_SMD_Abracon_ASE-4Pin_3.2x2.5mm": lambda block: { + "1": block.crystal.xtal_in, + "2": block.gnd, + "3": block.crystal.xtal_out, + "4": block.gnd, + }, + "Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm": lambda block: { + "1": block.crystal.xtal_in, + "2": block.gnd, + "3": block.crystal.xtal_out, + "4": block.gnd, + }, + "Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm": lambda block: { + "1": block.crystal.xtal_in, + "2": block.gnd, + "3": block.crystal.xtal_out, + "4": block.gnd, + }, + } @non_library class TableCrystal(PartsTableSelector, Crystal): - FREQUENCY = PartsTableColumn(Range) - CAPACITANCE = PartsTableColumn(float) + FREQUENCY = PartsTableColumn(Range) + CAPACITANCE = PartsTableColumn(float) - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Discrete crystal component.""" - super().__init__(*args, **kwargs) - self.generator_param(self.frequency) + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Discrete crystal component.""" + super().__init__(*args, **kwargs) + self.generator_param(self.frequency) - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - (row[self.FREQUENCY] in self.get(self.frequency)) + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return super()._row_filter(row) and (row[self.FREQUENCY] in self.get(self.frequency)) - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_frequency, row[self.FREQUENCY]) - self.assign(self.actual_capacitance, row[self.CAPACITANCE]) + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_frequency, row[self.FREQUENCY]) + self.assign(self.actual_capacitance, row[self.CAPACITANCE]) @abstract_block_default(lambda: OscillatorCrystal) class OscillatorReference(DiscreteApplication): - def __init__(self, frequency: RangeLike) -> None: - """Crystal and supporting circuitry to connect it to an oscillator driver. - Should include load capacitors.""" - super().__init__() + def __init__(self, frequency: RangeLike) -> None: + """Crystal and supporting circuitry to connect it to an oscillator driver. + Should include load capacitors.""" + super().__init__() - self.crystal = self.Port(CrystalPort.empty(), [InOut]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.crystal = self.Port(CrystalPort.empty(), [InOut]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.frequency = self.ArgParameter(frequency) + self.frequency = self.ArgParameter(frequency) class OscillatorCrystal(OscillatorReference): # TODO rename to disambiguate from part? - """Crystal and supporting circuitry to connect it to an oscillator driver. - Should include load capacitors.""" - PARASITIC_CAPACITANCE = 5e-12 - - # Tolerance selected using table 32 in https://www.nxp.com/docs/en/data-sheet/LPC15XX.pdf, which gives suggested - # load cap values 18pF, 39pF, 57pF. Assume any capacitance in the center could round either way - 22pF goes to 18pF - # or 39pF, which gives a 28% tolerance. - # Calculation: for tolerance y and series multiplicative factor a, we need (1+y) = a(1-y) - # which solves to y=(a-1)/(a+1) - # Then stack an additive 10% tolerance for the capacitor tolerance, for a total 0.38 tolerance. - # TODO this should be formalized better. - CAPACITOR_TOLERANCE = 0.38 - - @override - def contents(self) -> None: - super().contents() - - self.package = self.Block(Crystal(self.frequency)) - - cap_model = Capacitor( - capacitance=( - (self.package.actual_capacitance - self.PARASITIC_CAPACITANCE) * 2 * (1 - self.CAPACITOR_TOLERANCE), - (self.package.actual_capacitance - self.PARASITIC_CAPACITANCE) * 2 * (1 + self.CAPACITOR_TOLERANCE)), - voltage=self.crystal.link().drive_voltage - ) - self.cap_a = self.Block(cap_model) - self.cap_b = self.Block(cap_model) - self.connect(self.crystal, self.package.crystal) - self.connect(self.crystal.xtal_in, self.cap_a.pos) - self.connect(self.crystal.xtal_out, self.cap_b.pos) - self.connect(self.gnd, self.cap_a.neg.adapt_to(Ground()), self.cap_b.neg.adapt_to(Ground()), self.package.gnd) + """Crystal and supporting circuitry to connect it to an oscillator driver. + Should include load capacitors.""" + + PARASITIC_CAPACITANCE = 5e-12 + + # Tolerance selected using table 32 in https://www.nxp.com/docs/en/data-sheet/LPC15XX.pdf, which gives suggested + # load cap values 18pF, 39pF, 57pF. Assume any capacitance in the center could round either way - 22pF goes to 18pF + # or 39pF, which gives a 28% tolerance. + # Calculation: for tolerance y and series multiplicative factor a, we need (1+y) = a(1-y) + # which solves to y=(a-1)/(a+1) + # Then stack an additive 10% tolerance for the capacitor tolerance, for a total 0.38 tolerance. + # TODO this should be formalized better. + CAPACITOR_TOLERANCE = 0.38 + + @override + def contents(self) -> None: + super().contents() + + self.package = self.Block(Crystal(self.frequency)) + + cap_model = Capacitor( + capacitance=( + (self.package.actual_capacitance - self.PARASITIC_CAPACITANCE) * 2 * (1 - self.CAPACITOR_TOLERANCE), + (self.package.actual_capacitance - self.PARASITIC_CAPACITANCE) * 2 * (1 + self.CAPACITOR_TOLERANCE), + ), + voltage=self.crystal.link().drive_voltage, + ) + self.cap_a = self.Block(cap_model) + self.cap_b = self.Block(cap_model) + self.connect(self.crystal, self.package.crystal) + self.connect(self.crystal.xtal_in, self.cap_a.pos) + self.connect(self.crystal.xtal_out, self.cap_b.pos) + self.connect(self.gnd, self.cap_a.neg.adapt_to(Ground()), self.cap_b.neg.adapt_to(Ground()), self.package.gnd) class CeramicResonator(OscillatorReference): - """Category for ceramic resonators""" + """Category for ceramic resonators""" diff --git a/edg/abstract_parts/AbstractDebugHeaders.py b/edg/abstract_parts/AbstractDebugHeaders.py index 485a05e71..d2467a8ad 100644 --- a/edg/abstract_parts/AbstractDebugHeaders.py +++ b/edg/abstract_parts/AbstractDebugHeaders.py @@ -6,31 +6,35 @@ @abstract_block class SwdCortexTargetConnector(ProgrammingConnector): - """Programming header with power and SWD (SWCLK/SWDIO/RESET) pins.""" - def __init__(self) -> None: - super().__init__() + """Programming header with power and SWD (SWCLK/SWDIO/RESET) pins.""" - self.pwr = self.Port(VoltageSink.empty(), [Power]) # in practice these are commonly used as sources - self.gnd = self.Port(Ground.empty(), [Common]) - self.swd = self.Port(SwdHostPort.empty(), [Output]) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) # in practice these are commonly used as sources + self.gnd = self.Port(Ground.empty(), [Common]) + self.swd = self.Port(SwdHostPort.empty(), [Output]) class SwdCortexTargetConnectorReset(BlockInterfaceMixin[SwdCortexTargetConnector]): - """Mixin for SWD connectors with adding the optional reset pin""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.reset = self.Port(DigitalSource.empty(), optional=True) # as open-drain + """Mixin for SWD connectors with adding the optional reset pin""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.reset = self.Port(DigitalSource.empty(), optional=True) # as open-drain class SwdCortexTargetConnectorSwo(BlockInterfaceMixin[SwdCortexTargetConnector]): - """Mixin for SWD connectors with adding the optional SWO pin""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.swo = self.Port(DigitalBidir.empty(), optional=True) + """Mixin for SWD connectors with adding the optional SWO pin""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.swo = self.Port(DigitalBidir.empty(), optional=True) class SwdCortexTargetConnectorTdi(BlockInterfaceMixin[SwdCortexTargetConnector]): - """Mixin for SWD connectors with adding the NONSTANDARD TDI pin (where pins are shared with JTAG)""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.tdi = self.Port(DigitalBidir.empty(), optional=True) + """Mixin for SWD connectors with adding the NONSTANDARD TDI pin (where pins are shared with JTAG)""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.tdi = self.Port(DigitalBidir.empty(), optional=True) diff --git a/edg/abstract_parts/AbstractDevices.py b/edg/abstract_parts/AbstractDevices.py index c45ca0b64..4bde4a1af 100644 --- a/edg/abstract_parts/AbstractDevices.py +++ b/edg/abstract_parts/AbstractDevices.py @@ -4,18 +4,16 @@ @abstract_block class Battery(PowerSource): - def __init__(self, voltage: RangeLike, - current: RangeLike = RangeExpr.ZERO, *, - capacity: FloatLike = 0.0): - super().__init__() + def __init__(self, voltage: RangeLike, current: RangeLike = RangeExpr.ZERO, *, capacity: FloatLike = 0.0): + super().__init__() - self.pwr = self.Port(VoltageSource.empty()) # set by subclasses - self.gnd = self.Port(Ground.empty()) + self.pwr = self.Port(VoltageSource.empty()) # set by subclasses + self.gnd = self.Port(Ground.empty()) - self.voltage = self.ArgParameter(voltage) - self.capacity = self.ArgParameter(capacity) - self.actual_capacity = self.Parameter(RangeExpr()) + self.voltage = self.ArgParameter(voltage) + self.capacity = self.ArgParameter(capacity) + self.actual_capacity = self.Parameter(RangeExpr()) - self.require(self.pwr.voltage_out.within(voltage + self.gnd.link().voltage)) - self.require(self.pwr.current_limits.contains(current)) - self.require(self.actual_capacity.upper() >= capacity) + self.require(self.pwr.voltage_out.within(voltage + self.gnd.link().voltage)) + self.require(self.pwr.current_limits.contains(current)) + self.require(self.actual_capacity.upper() >= capacity) diff --git a/edg/abstract_parts/AbstractDiodes.py b/edg/abstract_parts/AbstractDiodes.py index d2aa1e79c..1f9f76144 100644 --- a/edg/abstract_parts/AbstractDiodes.py +++ b/edg/abstract_parts/AbstractDiodes.py @@ -12,210 +12,240 @@ @non_library class BaseDiode(DiscreteSemiconductor, HasStandardFootprint): - """Base class for diodes, with anode and cathode pins, including a very wide range of devices. - """ - _STANDARD_FOOTPRINT = lambda: DiodeStandardFootprint - - def __init__(self) -> None: - super().__init__() - - self.anode = self.Port(Passive.empty()) - self.cathode = self.Port(Passive.empty()) - - -class DiodeStandardFootprint(StandardFootprint['BaseDiode']): - REFDES_PREFIX = 'D' - - FOOTPRINT_PINNING_MAP = { - ( - 'Diode_SMD:D_MiniMELF', - 'Diode_SMD:D_SOD-123', - 'Diode_SMD:D_SOD-323', - 'Diode_SMD:D_SMA', - 'Diode_SMD:D_SMB', - 'Diode_SMD:D_SMC', - ): lambda block: { - '1': block.cathode, - '2': block.anode, - }, - ( # TODO are these standard? - 'Package_TO_SOT_SMD:TO-252-2', - 'Package_TO_SOT_SMD:TO-263-2', - ): lambda block: { - '1': block.anode, # sometimes NC - '2': block.cathode, - '3': block.anode, - }, - } + """Base class for diodes, with anode and cathode pins, including a very wide range of devices.""" + + _STANDARD_FOOTPRINT = lambda: DiodeStandardFootprint + + def __init__(self) -> None: + super().__init__() + + self.anode = self.Port(Passive.empty()) + self.cathode = self.Port(Passive.empty()) + + +class DiodeStandardFootprint(StandardFootprint["BaseDiode"]): + REFDES_PREFIX = "D" + + FOOTPRINT_PINNING_MAP = { + ( + "Diode_SMD:D_MiniMELF", + "Diode_SMD:D_SOD-123", + "Diode_SMD:D_SOD-323", + "Diode_SMD:D_SMA", + "Diode_SMD:D_SMB", + "Diode_SMD:D_SMC", + ): lambda block: { + "1": block.cathode, + "2": block.anode, + }, + ( # TODO are these standard? + "Package_TO_SOT_SMD:TO-252-2", + "Package_TO_SOT_SMD:TO-263-2", + ): lambda block: { + "1": block.anode, # sometimes NC + "2": block.cathode, + "3": block.anode, + }, + } @abstract_block class Diode(KiCadImportableBlock, BaseDiode): - """Base class for untyped diodes - - TODO power? capacitance? leakage current? - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name in ('Device:D', 'Device:D_Small') - return {'A': self.anode, 'K': self.cathode} - - def __init__(self, reverse_voltage: RangeLike, current: RangeLike, *, - voltage_drop: RangeLike = Range.all(), - reverse_recovery_time: RangeLike = Range.all()) -> None: - super().__init__() - - self.reverse_voltage = self.ArgParameter(reverse_voltage) - self.current = self.ArgParameter(current) - self.voltage_drop = self.ArgParameter(voltage_drop) - self.reverse_recovery_time = self.ArgParameter(reverse_recovery_time) - - self.actual_voltage_rating = self.Parameter(RangeExpr()) - self.actual_current_rating = self.Parameter(RangeExpr()) - self.actual_voltage_drop = self.Parameter(RangeExpr()) - self.actual_reverse_recovery_time = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "Vr: ", DescriptionString.FormatUnits(self.actual_voltage_rating, "V"), - " of operating: ", DescriptionString.FormatUnits(self.reverse_voltage, "V"), "\n", - "If: ", DescriptionString.FormatUnits(self.actual_current_rating, "A"), - " of operating: ", DescriptionString.FormatUnits(self.current, "A"), "\n", - "Vf: ", DescriptionString.FormatUnits(self.actual_voltage_drop, "V"), - " of spec: ", DescriptionString.FormatUnits(self.voltage_drop, "V") - ) + """Base class for untyped diodes + + TODO power? capacitance? leakage current? + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name in ("Device:D", "Device:D_Small") + return {"A": self.anode, "K": self.cathode} + + def __init__( + self, + reverse_voltage: RangeLike, + current: RangeLike, + *, + voltage_drop: RangeLike = Range.all(), + reverse_recovery_time: RangeLike = Range.all(), + ) -> None: + super().__init__() + + self.reverse_voltage = self.ArgParameter(reverse_voltage) + self.current = self.ArgParameter(current) + self.voltage_drop = self.ArgParameter(voltage_drop) + self.reverse_recovery_time = self.ArgParameter(reverse_recovery_time) + + self.actual_voltage_rating = self.Parameter(RangeExpr()) + self.actual_current_rating = self.Parameter(RangeExpr()) + self.actual_voltage_drop = self.Parameter(RangeExpr()) + self.actual_reverse_recovery_time = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "Vr: ", + DescriptionString.FormatUnits(self.actual_voltage_rating, "V"), + " of operating: ", + DescriptionString.FormatUnits(self.reverse_voltage, "V"), + "\n", + "If: ", + DescriptionString.FormatUnits(self.actual_current_rating, "A"), + " of operating: ", + DescriptionString.FormatUnits(self.current, "A"), + "\n", + "Vf: ", + DescriptionString.FormatUnits(self.actual_voltage_drop, "V"), + " of spec: ", + DescriptionString.FormatUnits(self.voltage_drop, "V"), + ) @non_library class TableDiode(PartsTableSelector, Diode): - VOLTAGE_RATING = PartsTableColumn(Range) # tolerable blocking voltages, positive - CURRENT_RATING = PartsTableColumn(Range) # tolerable currents, average - FORWARD_VOLTAGE = PartsTableColumn(Range) # possible forward voltage range - REVERSE_RECOVERY = PartsTableColumn(Range) # possible reverse recovery time - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.reverse_voltage, self.current, self.voltage_drop, self.reverse_recovery_time) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - self.get(self.reverse_voltage).fuzzy_in(row[self.VOLTAGE_RATING]) and \ - self.get(self.current).fuzzy_in(row[self.CURRENT_RATING]) and \ - row[self.FORWARD_VOLTAGE].fuzzy_in(self.get(self.voltage_drop)) and \ - row[self.REVERSE_RECOVERY].fuzzy_in(self.get(self.reverse_recovery_time)) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) - self.assign(self.actual_current_rating, row[self.CURRENT_RATING]) - self.assign(self.actual_voltage_drop, row[self.FORWARD_VOLTAGE]) - self.assign(self.actual_reverse_recovery_time, row[self.REVERSE_RECOVERY]) + VOLTAGE_RATING = PartsTableColumn(Range) # tolerable blocking voltages, positive + CURRENT_RATING = PartsTableColumn(Range) # tolerable currents, average + FORWARD_VOLTAGE = PartsTableColumn(Range) # possible forward voltage range + REVERSE_RECOVERY = PartsTableColumn(Range) # possible reverse recovery time + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.reverse_voltage, self.current, self.voltage_drop, self.reverse_recovery_time) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return ( + super()._row_filter(row) + and self.get(self.reverse_voltage).fuzzy_in(row[self.VOLTAGE_RATING]) + and self.get(self.current).fuzzy_in(row[self.CURRENT_RATING]) + and row[self.FORWARD_VOLTAGE].fuzzy_in(self.get(self.voltage_drop)) + and row[self.REVERSE_RECOVERY].fuzzy_in(self.get(self.reverse_recovery_time)) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) + self.assign(self.actual_current_rating, row[self.CURRENT_RATING]) + self.assign(self.actual_voltage_drop, row[self.FORWARD_VOLTAGE]) + self.assign(self.actual_reverse_recovery_time, row[self.REVERSE_RECOVERY]) @abstract_block class ZenerDiode(KiCadImportableBlock, BaseDiode, DiscreteSemiconductor): - """Base class for untyped zeners + """Base class for untyped zeners - TODO power? capacitance? leakage current? - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name in ('Device:D_Zener', 'Device:D_Zener_Small') - return {'A': self.anode, 'K': self.cathode} + TODO power? capacitance? leakage current? + """ - def __init__(self, zener_voltage: RangeLike) -> None: - super().__init__() + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name in ("Device:D_Zener", "Device:D_Zener_Small") + return {"A": self.anode, "K": self.cathode} - self.zener_voltage = self.ArgParameter(zener_voltage) + def __init__(self, zener_voltage: RangeLike) -> None: + super().__init__() - self.actual_zener_voltage = self.Parameter(RangeExpr()) - self.actual_power_rating = self.Parameter(RangeExpr()) + self.zener_voltage = self.ArgParameter(zener_voltage) - @override - def contents(self) -> None: - super().contents() + self.actual_zener_voltage = self.Parameter(RangeExpr()) + self.actual_power_rating = self.Parameter(RangeExpr()) - self.description = DescriptionString( - "zener voltage=", DescriptionString.FormatUnits(self.actual_zener_voltage, "V"), - " of spec:", DescriptionString.FormatUnits(self.zener_voltage, "V"), "\n", - "power=", DescriptionString.FormatUnits(self.actual_power_rating, "W") - ) + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "zener voltage=", + DescriptionString.FormatUnits(self.actual_zener_voltage, "V"), + " of spec:", + DescriptionString.FormatUnits(self.zener_voltage, "V"), + "\n", + "power=", + DescriptionString.FormatUnits(self.actual_power_rating, "W"), + ) @non_library class TableZenerDiode(PartsTableSelector, ZenerDiode): - ZENER_VOLTAGE = PartsTableColumn(Range) - POWER_RATING = PartsTableColumn(Range) # tolerable power + ZENER_VOLTAGE = PartsTableColumn(Range) + POWER_RATING = PartsTableColumn(Range) # tolerable power - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.zener_voltage) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.zener_voltage) - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - row[self.ZENER_VOLTAGE].fuzzy_in(self.get(self.zener_voltage)) + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return super()._row_filter(row) and row[self.ZENER_VOLTAGE].fuzzy_in(self.get(self.zener_voltage)) - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_zener_voltage, row[self.ZENER_VOLTAGE]) - self.assign(self.actual_power_rating, row[self.POWER_RATING]) + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_zener_voltage, row[self.ZENER_VOLTAGE]) + self.assign(self.actual_power_rating, row[self.POWER_RATING]) class ProtectionZenerDiode(Protection): - """Zener diode reversed across a power rail to provide transient overvoltage protection (and become an incandescent - indicator on a reverse voltage)""" - def __init__(self, voltage: RangeLike): - super().__init__() + """Zener diode reversed across a power rail to provide transient overvoltage protection (and become an incandescent + indicator on a reverse voltage)""" + + def __init__(self, voltage: RangeLike): + super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power, InOut]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Port(VoltageSink.empty(), [Power, InOut]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.voltage = self.ArgParameter(voltage) + self.voltage = self.ArgParameter(voltage) - @override - def contents(self) -> None: - super().contents() - self.diode = self.Block(ZenerDiode(zener_voltage=self.voltage)) - self.connect(self.diode.cathode.adapt_to(VoltageSink( - voltage_limits=(0, self.diode.actual_zener_voltage.lower()), - )), self.pwr) - self.connect(self.diode.anode.adapt_to(Ground()), self.gnd) + @override + def contents(self) -> None: + super().contents() + self.diode = self.Block(ZenerDiode(zener_voltage=self.voltage)) + self.connect( + self.diode.cathode.adapt_to( + VoltageSink( + voltage_limits=(0, self.diode.actual_zener_voltage.lower()), + ) + ), + self.pwr, + ) + self.connect(self.diode.anode.adapt_to(Ground()), self.gnd) @deprecated("Use AnalogClampResistor, which should be cheaper and cause less signal distortion") class AnalogClampZenerDiode(Protection, KiCadImportableBlock): - """Analog overvoltage protection diode to clamp the input voltage""" - def __init__(self, voltage: RangeLike): - super().__init__() - - self.signal_in = self.Port(AnalogSink.empty(), [Input]) - self.signal_out = self.Port(AnalogSource.empty(), [Output]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.voltage = self.ArgParameter(voltage) - - @override - def contents(self) -> None: - super().contents() - - self.diode = self.Block(ZenerDiode(zener_voltage=self.voltage)) - - self.forced = self.Block(ForcedAnalogVoltage( - forced_voltage=self.signal_in.link().voltage.intersect( - self.gnd.link().voltage + (0, self.diode.actual_zener_voltage.upper())) - )) - self.connect(self.signal_in, self.forced.signal_in) - self.connect(self.signal_out, self.forced.signal_out, self.diode.cathode.adapt_to(AnalogSink())) - self.connect(self.diode.anode.adapt_to(Ground()), self.gnd) - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: - assert symbol_name == 'edg_importable:AnalogClampZenerDiode' - return {'IN': self.signal_in, 'OUT': self.signal_out, 'GND': self.gnd} + """Analog overvoltage protection diode to clamp the input voltage""" + + def __init__(self, voltage: RangeLike): + super().__init__() + + self.signal_in = self.Port(AnalogSink.empty(), [Input]) + self.signal_out = self.Port(AnalogSource.empty(), [Output]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.voltage = self.ArgParameter(voltage) + + @override + def contents(self) -> None: + super().contents() + + self.diode = self.Block(ZenerDiode(zener_voltage=self.voltage)) + + self.forced = self.Block( + ForcedAnalogVoltage( + forced_voltage=self.signal_in.link().voltage.intersect( + self.gnd.link().voltage + (0, self.diode.actual_zener_voltage.upper()) + ) + ) + ) + self.connect(self.signal_in, self.forced.signal_in) + self.connect(self.signal_out, self.forced.signal_out, self.diode.cathode.adapt_to(AnalogSink())) + self.connect(self.diode.anode.adapt_to(Ground()), self.gnd) + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: + assert symbol_name == "edg_importable:AnalogClampZenerDiode" + return {"IN": self.signal_in, "OUT": self.signal_out, "GND": self.gnd} diff --git a/edg/abstract_parts/AbstractFerriteBead.py b/edg/abstract_parts/AbstractFerriteBead.py index a8eedd32c..196c930e7 100644 --- a/edg/abstract_parts/AbstractFerriteBead.py +++ b/edg/abstract_parts/AbstractFerriteBead.py @@ -11,121 +11,143 @@ @abstract_block class FerriteBead(PassiveComponent, KiCadImportableBlock, HasStandardFootprint): - _STANDARD_FOOTPRINT = lambda: FerriteBeadStandardFootprint - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name in ('Device:L_Ferrite', 'Device:L_Ferrite_Small') - return {'1': self.a, '2': self.b} - - def __init__(self, *, current: RangeLike = RangeExpr.ZERO, - hf_impedance: RangeLike = RangeExpr.ALL, - dc_resistance: RangeLike = RangeExpr.ALL) -> None: - super().__init__() - - self.a = self.Port(Passive.empty()) - self.b = self.Port(Passive.empty()) - - self.current = self.ArgParameter(current) # operating current range - self.hf_impedance = self.ArgParameter(hf_impedance) - self.dc_resistance = self.ArgParameter(dc_resistance) - self.actual_current_rating = self.Parameter(RangeExpr()) - self.actual_hf_impedance = self.Parameter(RangeExpr()) - self.actual_dc_resistance = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "current rating: ", DescriptionString.FormatUnits(self.actual_current_rating, "A"), - " of operating ", DescriptionString.FormatUnits(self.current, "A"), "\n", - "HF impedance: ", DescriptionString.FormatUnits(self.actual_hf_impedance, "Ω"), - " of spec: ", DescriptionString.FormatUnits(self.hf_impedance, "Ω"), "\n", - "DC resistance: ", DescriptionString.FormatUnits(self.actual_dc_resistance, "Ω"), - " of spec: ", DescriptionString.FormatUnits(self.dc_resistance, "Ω") - ) + _STANDARD_FOOTPRINT = lambda: FerriteBeadStandardFootprint + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name in ("Device:L_Ferrite", "Device:L_Ferrite_Small") + return {"1": self.a, "2": self.b} + + def __init__( + self, + *, + current: RangeLike = RangeExpr.ZERO, + hf_impedance: RangeLike = RangeExpr.ALL, + dc_resistance: RangeLike = RangeExpr.ALL, + ) -> None: + super().__init__() + + self.a = self.Port(Passive.empty()) + self.b = self.Port(Passive.empty()) + + self.current = self.ArgParameter(current) # operating current range + self.hf_impedance = self.ArgParameter(hf_impedance) + self.dc_resistance = self.ArgParameter(dc_resistance) + self.actual_current_rating = self.Parameter(RangeExpr()) + self.actual_hf_impedance = self.Parameter(RangeExpr()) + self.actual_dc_resistance = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "current rating: ", + DescriptionString.FormatUnits(self.actual_current_rating, "A"), + " of operating ", + DescriptionString.FormatUnits(self.current, "A"), + "\n", + "HF impedance: ", + DescriptionString.FormatUnits(self.actual_hf_impedance, "Ω"), + " of spec: ", + DescriptionString.FormatUnits(self.hf_impedance, "Ω"), + "\n", + "DC resistance: ", + DescriptionString.FormatUnits(self.actual_dc_resistance, "Ω"), + " of spec: ", + DescriptionString.FormatUnits(self.dc_resistance, "Ω"), + ) class FerriteBeadStandardFootprint(StandardFootprint[FerriteBead]): - REFDES_PREFIX = 'FB' - - FOOTPRINT_PINNING_MAP = { - ( - 'Inductor_SMD:L_0201_0603Metric', - 'Inductor_SMD:L_0402_1005Metric', - 'Inductor_SMD:L_0603_1608Metric', - 'Inductor_SMD:L_0805_2012Metric', - 'Inductor_SMD:L_1206_3216Metric', - 'Inductor_SMD:L_1210_3225Metric', - 'Inductor_SMD:L_1812_4532Metric', - 'Inductor_SMD:L_2010_5025Metric', - 'Inductor_SMD:L_2512_6332Metric', - ): lambda block: { - '1': block.a, - '2': block.b, - }, - } + REFDES_PREFIX = "FB" + + FOOTPRINT_PINNING_MAP = { + ( + "Inductor_SMD:L_0201_0603Metric", + "Inductor_SMD:L_0402_1005Metric", + "Inductor_SMD:L_0603_1608Metric", + "Inductor_SMD:L_0805_2012Metric", + "Inductor_SMD:L_1206_3216Metric", + "Inductor_SMD:L_1210_3225Metric", + "Inductor_SMD:L_1812_4532Metric", + "Inductor_SMD:L_2010_5025Metric", + "Inductor_SMD:L_2512_6332Metric", + ): lambda block: { + "1": block.a, + "2": block.b, + }, + } @non_library class TableFerriteBead(PartsTableSelector, FerriteBead): - CURRENT_RATING = PartsTableColumn(Range) - HF_IMPEDANCE = PartsTableColumn(Range) - DC_RESISTANCE = PartsTableColumn(Range) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.current, self.hf_impedance, self.dc_resistance) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - self.get(self.current).fuzzy_in(row[self.CURRENT_RATING]) and \ - row[self.HF_IMPEDANCE].fuzzy_in(self.get(self.hf_impedance)) and \ - row[self.DC_RESISTANCE].fuzzy_in(self.get(self.dc_resistance)) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_current_rating, row[self.CURRENT_RATING]) - self.assign(self.actual_hf_impedance, row[self.HF_IMPEDANCE]) - self.assign(self.actual_dc_resistance, row[self.DC_RESISTANCE]) + CURRENT_RATING = PartsTableColumn(Range) + HF_IMPEDANCE = PartsTableColumn(Range) + DC_RESISTANCE = PartsTableColumn(Range) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.current, self.hf_impedance, self.dc_resistance) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return ( + super()._row_filter(row) + and self.get(self.current).fuzzy_in(row[self.CURRENT_RATING]) + and row[self.HF_IMPEDANCE].fuzzy_in(self.get(self.hf_impedance)) + and row[self.DC_RESISTANCE].fuzzy_in(self.get(self.dc_resistance)) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_current_rating, row[self.CURRENT_RATING]) + self.assign(self.actual_hf_impedance, row[self.HF_IMPEDANCE]) + self.assign(self.actual_dc_resistance, row[self.DC_RESISTANCE]) class SeriesPowerFerriteBead(DiscreteApplication, KiCadImportableBlock): - """Series ferrite bead for power applications""" - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name in ('Device:L_Ferrite', 'Device:L_Ferrite_Small') - return {'1': self.pwr_in, '2': self.pwr_out} - - def __init__(self, hf_impedance: RangeLike = RangeExpr.ALL, - dc_resistance: RangeLike = RangeExpr.ALL) -> None: - super().__init__() - - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration - self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration - - self.fb = self.Block(FerriteBead( - current=self.pwr_out.link().current_drawn, - hf_impedance=hf_impedance, - dc_resistance=dc_resistance - )) - self.connect(self.pwr_in, self.fb.a.adapt_to(VoltageSink( - voltage_limits=Range.all(), # ideal - current_draw=self.pwr_out.link().current_drawn - ))) - self.connect(self.pwr_out, self.fb.b.adapt_to(VoltageSource( - voltage_out=self.pwr_in.link().voltage, # ignore voltage drop - current_limits=self.fb.actual_current_rating - ))) - - def connected(self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None) -> \ - 'SeriesPowerFerriteBead': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if pwr_in is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) - if pwr_out is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) - return self + """Series ferrite bead for power applications""" + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name in ("Device:L_Ferrite", "Device:L_Ferrite_Small") + return {"1": self.pwr_in, "2": self.pwr_out} + + def __init__(self, hf_impedance: RangeLike = RangeExpr.ALL, dc_resistance: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration + self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration + + self.fb = self.Block( + FerriteBead( + current=self.pwr_out.link().current_drawn, hf_impedance=hf_impedance, dc_resistance=dc_resistance + ) + ) + self.connect( + self.pwr_in, + self.fb.a.adapt_to( + VoltageSink(voltage_limits=Range.all(), current_draw=self.pwr_out.link().current_drawn) # ideal + ), + ) + self.connect( + self.pwr_out, + self.fb.b.adapt_to( + VoltageSource( + voltage_out=self.pwr_in.link().voltage, # ignore voltage drop + current_limits=self.fb.actual_current_rating, + ) + ), + ) + + def connected( + self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None + ) -> "SeriesPowerFerriteBead": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if pwr_in is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) + if pwr_out is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) + return self diff --git a/edg/abstract_parts/AbstractFets.py b/edg/abstract_parts/AbstractFets.py index f18ee6da2..13f3307cd 100644 --- a/edg/abstract_parts/AbstractFets.py +++ b/edg/abstract_parts/AbstractFets.py @@ -11,279 +11,333 @@ @abstract_block class Fet(KiCadImportableBlock, DiscreteSemiconductor, HasStandardFootprint): - """Base class for untyped MOSFETs - Drain voltage, drain current, and gate voltages are positive (absolute). - - The gate voltage is only checked against maximum ratings. - Optionally, the gate threshold voltage can also be specified. - - The actual gate drive voltage is specified as (threshold voltage, gate drive voltage), where the top end of that - is either the voltage at Rds,on or the specified driving voltage level. - - MOSFET equations - - https://inst.eecs.berkeley.edu/~ee105/fa05/handouts/discussions/Discussion5.pdf (cutoff/linear/saturation regions) - - Potentially useful references for selecting FETs: - - Toshiba application_note_en_20180726, Power MOSFET Selecting MOSFFETs and Consideration for Circuit Design - - https://www.vishay.com/docs/71933/71933.pdf, MOSFET figures of merit (which don't help in choosing devices), Rds,on * Qg - - https://www.allaboutcircuits.com/technical-articles/choosing-the-right-transistor-understanding-low-frequency-mosfet-parameters/ - - https://www.allaboutcircuits.com/technical-articles/choosing-the-right-transistor-understanding-dynamic-mosfet-parameters/ - """ - _STANDARD_FOOTPRINT = lambda: FetStandardFootprint - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - # TODO actually check that the device channel corresponds with the schematic? - assert symbol_name.startswith('Device:Q_NMOS_') or symbol_name.startswith('Device:Q_PMOS_') - assert symbol_name.removeprefix('Device:Q_NMOS_').removeprefix('Device:Q_PMOS_') in \ - ('DGS', 'DSG', 'GDS', 'GSD', 'SDG', 'SGD') - return {'D': self.drain, 'G': self.gate, 'S': self.source} - - @staticmethod - def NFet(*args: Any, **kwargs: Any) -> 'Fet': - return Fet(*args, **kwargs, channel='N') - - @staticmethod - def PFet(*args: Any, **kwargs: Any) -> 'Fet': - return Fet(*args, **kwargs, channel='P') - - def __init__(self, drain_voltage: RangeLike, drain_current: RangeLike, *, - gate_voltage: RangeLike = (0, 0), gate_threshold_voltage: RangeLike = Range.all(), - rds_on: RangeLike = Range.all(), - gate_charge: RangeLike = Range.all(), power: RangeLike = Range.exact(0), - channel: StringLike = StringExpr()) -> None: - super().__init__() - - self.source = self.Port(Passive.empty()) - self.drain = self.Port(Passive.empty()) - self.gate = self.Port(Passive.empty()) - - self.drain_voltage = self.ArgParameter(drain_voltage) - self.drain_current = self.ArgParameter(drain_current) - self.gate_voltage = self.ArgParameter(gate_voltage) - self.gate_threshold_voltage = self.ArgParameter(gate_threshold_voltage) - self.rds_on = self.ArgParameter(rds_on) - self.gate_charge = self.ArgParameter(gate_charge) - self.power = self.ArgParameter(power) - self.channel = self.ArgParameter(channel) - - self.actual_drain_voltage_rating = self.Parameter(RangeExpr()) - self.actual_drain_current_rating = self.Parameter(RangeExpr()) - self.actual_gate_voltage_rating = self.Parameter(RangeExpr()) - self.actual_gate_drive = self.Parameter(RangeExpr()) - self.actual_power_rating = self.Parameter(RangeExpr()) - self.actual_rds_on = self.Parameter(RangeExpr()) - self.actual_gate_charge = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "Vds: ", DescriptionString.FormatUnits(self.actual_drain_voltage_rating, "V"), - " of operating: ", DescriptionString.FormatUnits(self.drain_voltage, "V"), "\n", - "Ids: ", DescriptionString.FormatUnits(self.actual_drain_current_rating, "A"), - " of operating: ", DescriptionString.FormatUnits(self.drain_current, "A"), "\n", - "Vgs,max: ", DescriptionString.FormatUnits(self.actual_gate_voltage_rating, "V"), - " of operating: ", DescriptionString.FormatUnits(self.gate_voltage, "V"), "\n", - "Vgs,th: ", DescriptionString.FormatUnits(self.actual_gate_drive, "V"), "\n", - "Rds,on: ", DescriptionString.FormatUnits(self.actual_rds_on, "Ω"), - " of spec: ", DescriptionString.FormatUnits(self.rds_on, "Ω"), "\n" - "Pmax: ", DescriptionString.FormatUnits(self.actual_power_rating, "W"), - " of operating: ", DescriptionString.FormatUnits(self.power, "W") - ) + """Base class for untyped MOSFETs + Drain voltage, drain current, and gate voltages are positive (absolute). + + The gate voltage is only checked against maximum ratings. + Optionally, the gate threshold voltage can also be specified. + + The actual gate drive voltage is specified as (threshold voltage, gate drive voltage), where the top end of that + is either the voltage at Rds,on or the specified driving voltage level. + + MOSFET equations + - https://inst.eecs.berkeley.edu/~ee105/fa05/handouts/discussions/Discussion5.pdf (cutoff/linear/saturation regions) + + Potentially useful references for selecting FETs: + - Toshiba application_note_en_20180726, Power MOSFET Selecting MOSFFETs and Consideration for Circuit Design + - https://www.vishay.com/docs/71933/71933.pdf, MOSFET figures of merit (which don't help in choosing devices), Rds,on * Qg + - https://www.allaboutcircuits.com/technical-articles/choosing-the-right-transistor-understanding-low-frequency-mosfet-parameters/ + - https://www.allaboutcircuits.com/technical-articles/choosing-the-right-transistor-understanding-dynamic-mosfet-parameters/ + """ + + _STANDARD_FOOTPRINT = lambda: FetStandardFootprint + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + # TODO actually check that the device channel corresponds with the schematic? + assert symbol_name.startswith("Device:Q_NMOS_") or symbol_name.startswith("Device:Q_PMOS_") + assert symbol_name.removeprefix("Device:Q_NMOS_").removeprefix("Device:Q_PMOS_") in ( + "DGS", + "DSG", + "GDS", + "GSD", + "SDG", + "SGD", + ) + return {"D": self.drain, "G": self.gate, "S": self.source} + + @staticmethod + def NFet(*args: Any, **kwargs: Any) -> "Fet": + return Fet(*args, **kwargs, channel="N") + + @staticmethod + def PFet(*args: Any, **kwargs: Any) -> "Fet": + return Fet(*args, **kwargs, channel="P") + + def __init__( + self, + drain_voltage: RangeLike, + drain_current: RangeLike, + *, + gate_voltage: RangeLike = (0, 0), + gate_threshold_voltage: RangeLike = Range.all(), + rds_on: RangeLike = Range.all(), + gate_charge: RangeLike = Range.all(), + power: RangeLike = Range.exact(0), + channel: StringLike = StringExpr(), + ) -> None: + super().__init__() + + self.source = self.Port(Passive.empty()) + self.drain = self.Port(Passive.empty()) + self.gate = self.Port(Passive.empty()) + + self.drain_voltage = self.ArgParameter(drain_voltage) + self.drain_current = self.ArgParameter(drain_current) + self.gate_voltage = self.ArgParameter(gate_voltage) + self.gate_threshold_voltage = self.ArgParameter(gate_threshold_voltage) + self.rds_on = self.ArgParameter(rds_on) + self.gate_charge = self.ArgParameter(gate_charge) + self.power = self.ArgParameter(power) + self.channel = self.ArgParameter(channel) + + self.actual_drain_voltage_rating = self.Parameter(RangeExpr()) + self.actual_drain_current_rating = self.Parameter(RangeExpr()) + self.actual_gate_voltage_rating = self.Parameter(RangeExpr()) + self.actual_gate_drive = self.Parameter(RangeExpr()) + self.actual_power_rating = self.Parameter(RangeExpr()) + self.actual_rds_on = self.Parameter(RangeExpr()) + self.actual_gate_charge = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "Vds: ", + DescriptionString.FormatUnits(self.actual_drain_voltage_rating, "V"), + " of operating: ", + DescriptionString.FormatUnits(self.drain_voltage, "V"), + "\n", + "Ids: ", + DescriptionString.FormatUnits(self.actual_drain_current_rating, "A"), + " of operating: ", + DescriptionString.FormatUnits(self.drain_current, "A"), + "\n", + "Vgs,max: ", + DescriptionString.FormatUnits(self.actual_gate_voltage_rating, "V"), + " of operating: ", + DescriptionString.FormatUnits(self.gate_voltage, "V"), + "\n", + "Vgs,th: ", + DescriptionString.FormatUnits(self.actual_gate_drive, "V"), + "\n", + "Rds,on: ", + DescriptionString.FormatUnits(self.actual_rds_on, "Ω"), + " of spec: ", + DescriptionString.FormatUnits(self.rds_on, "Ω"), + "\n" "Pmax: ", + DescriptionString.FormatUnits(self.actual_power_rating, "W"), + " of operating: ", + DescriptionString.FormatUnits(self.power, "W"), + ) class FetStandardFootprint(StandardFootprint[Fet]): - REFDES_PREFIX = 'Q' - - FOOTPRINT_PINNING_MAP = { - ( - 'Package_TO_SOT_SMD:SOT-23', - 'Package_TO_SOT_SMD:SOT-323_SC-70', - ): lambda block: { - '1': block.gate, - '2': block.source, - '3': block.drain, - }, - ( - 'Package_TO_SOT_SMD:SOT-223-3_TabPin2', - 'Package_TO_SOT_SMD:TO-252-2', - 'Package_TO_SOT_SMD:TO-263-2' - ): lambda block: { - '1': block.gate, - '2': block.drain, - '3': block.source, - }, - 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm': lambda block: { - '1': block.source, - '2': block.source, - '3': block.source, - '4': block.gate, - '5': block.drain, - '6': block.drain, - '7': block.drain, - '8': block.drain, - }, - ( - 'Package_SO:PowerPAK_SO-8_Single', - 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - ): lambda block: { - '1': block.source, - '2': block.source, - '3': block.source, - '4': block.gate, - '5': block.drain, - }, - ( - 'Package_TO_SOT_THT:TO-220-3_Horizontal_TabUp', - 'Package_TO_SOT_THT:TO-220-3_Horizontal_TabDown', - 'Package_TO_SOT_THT:TO-220-3_Vertical', - ): lambda block: { - '1': block.gate, - '2': block.drain, - '3': block.source, - }, - } + REFDES_PREFIX = "Q" + + FOOTPRINT_PINNING_MAP = { + ( + "Package_TO_SOT_SMD:SOT-23", + "Package_TO_SOT_SMD:SOT-323_SC-70", + ): lambda block: { + "1": block.gate, + "2": block.source, + "3": block.drain, + }, + ( + "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + "Package_TO_SOT_SMD:TO-252-2", + "Package_TO_SOT_SMD:TO-263-2", + ): lambda block: { + "1": block.gate, + "2": block.drain, + "3": block.source, + }, + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm": lambda block: { + "1": block.source, + "2": block.source, + "3": block.source, + "4": block.gate, + "5": block.drain, + "6": block.drain, + "7": block.drain, + "8": block.drain, + }, + ( + "Package_SO:PowerPAK_SO-8_Single", + "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + ): lambda block: { + "1": block.source, + "2": block.source, + "3": block.source, + "4": block.gate, + "5": block.drain, + }, + ( + "Package_TO_SOT_THT:TO-220-3_Horizontal_TabUp", + "Package_TO_SOT_THT:TO-220-3_Horizontal_TabDown", + "Package_TO_SOT_THT:TO-220-3_Vertical", + ): lambda block: { + "1": block.gate, + "2": block.drain, + "3": block.source, + }, + } @non_library class BaseTableFet(Fet): - """Shared table columns for both TableFet and TableSwitchFet""" - VDS_RATING = PartsTableColumn(Range) - IDS_RATING = PartsTableColumn(Range) - VGS_RATING = PartsTableColumn(Range) - VGS_DRIVE = PartsTableColumn(Range) # negative for PMOS - RDS_ON = PartsTableColumn(Range) - POWER_RATING = PartsTableColumn(Range) - GATE_CHARGE = PartsTableColumn(Range) # units of C - CHANNEL = PartsTableColumn(str) # either 'P' or 'N' + """Shared table columns for both TableFet and TableSwitchFet""" + + VDS_RATING = PartsTableColumn(Range) + IDS_RATING = PartsTableColumn(Range) + VGS_RATING = PartsTableColumn(Range) + VGS_DRIVE = PartsTableColumn(Range) # negative for PMOS + RDS_ON = PartsTableColumn(Range) + POWER_RATING = PartsTableColumn(Range) + GATE_CHARGE = PartsTableColumn(Range) # units of C + CHANNEL = PartsTableColumn(str) # either 'P' or 'N' @non_library class TableFet(PartsTableSelector, BaseTableFet): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.drain_voltage, self.drain_current, self.gate_voltage, self.gate_threshold_voltage, - self.rds_on, self.gate_charge, self.power, self.channel) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - row[self.CHANNEL] == self.get(self.channel) and \ - self.get(self.drain_voltage).fuzzy_in(row[self.VDS_RATING]) and \ - self.get(self.drain_current).fuzzy_in(row[self.IDS_RATING]) and \ - self.get(self.gate_voltage).fuzzy_in(row[self.VGS_RATING]) and \ - (row[self.VGS_DRIVE].lower in self.get(self.gate_threshold_voltage)) and \ - row[self.RDS_ON].fuzzy_in(self.get(self.rds_on)) and \ - row[self.GATE_CHARGE].fuzzy_in(self.get(self.gate_charge)) and \ - self.get(self.power).fuzzy_in(row[self.POWER_RATING]) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_drain_voltage_rating, row[self.VDS_RATING]) - self.assign(self.actual_drain_current_rating, row[self.IDS_RATING]) - self.assign(self.actual_gate_voltage_rating, row[self.VGS_RATING]) - self.assign(self.actual_gate_drive, row[self.VGS_DRIVE]) - self.assign(self.actual_rds_on, row[self.RDS_ON]) - self.assign(self.actual_power_rating, row[self.POWER_RATING]) - self.assign(self.actual_gate_charge, row[self.GATE_CHARGE]) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param( + self.drain_voltage, + self.drain_current, + self.gate_voltage, + self.gate_threshold_voltage, + self.rds_on, + self.gate_charge, + self.power, + self.channel, + ) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return ( + super()._row_filter(row) + and row[self.CHANNEL] == self.get(self.channel) + and self.get(self.drain_voltage).fuzzy_in(row[self.VDS_RATING]) + and self.get(self.drain_current).fuzzy_in(row[self.IDS_RATING]) + and self.get(self.gate_voltage).fuzzy_in(row[self.VGS_RATING]) + and (row[self.VGS_DRIVE].lower in self.get(self.gate_threshold_voltage)) + and row[self.RDS_ON].fuzzy_in(self.get(self.rds_on)) + and row[self.GATE_CHARGE].fuzzy_in(self.get(self.gate_charge)) + and self.get(self.power).fuzzy_in(row[self.POWER_RATING]) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_drain_voltage_rating, row[self.VDS_RATING]) + self.assign(self.actual_drain_current_rating, row[self.IDS_RATING]) + self.assign(self.actual_gate_voltage_rating, row[self.VGS_RATING]) + self.assign(self.actual_gate_drive, row[self.VGS_DRIVE]) + self.assign(self.actual_rds_on, row[self.RDS_ON]) + self.assign(self.actual_power_rating, row[self.POWER_RATING]) + self.assign(self.actual_gate_charge, row[self.GATE_CHARGE]) @abstract_block class SwitchFet(Fet): - """FET that switches between an off state and on state, not operating in the linear region except for rise/fall time. - Ports remain untyped. TODO: are these limitations enough to type the ports? maybe except for the output? - Models static and switching power dissipation. Gate charge and power parameters are optional, they will be the - stricter of the explicit input or model-derived parameters.""" - # TODO ideally this would just instantaite a Fet internally, but the parts selection becomes more complex b/c - # parameters are cross-dependent - @staticmethod - @override - def NFet(*args: Any, **kwargs: Any) -> 'SwitchFet': - return SwitchFet(*args, **kwargs, channel='N') + """FET that switches between an off state and on state, not operating in the linear region except for rise/fall time. + Ports remain untyped. TODO: are these limitations enough to type the ports? maybe except for the output? + Models static and switching power dissipation. Gate charge and power parameters are optional, they will be the + stricter of the explicit input or model-derived parameters.""" - @staticmethod - @override - def PFet(*args: Any, **kwargs: Any) -> 'SwitchFet': - return SwitchFet(*args, **kwargs, channel='P') + # TODO ideally this would just instantaite a Fet internally, but the parts selection becomes more complex b/c + # parameters are cross-dependent + @staticmethod + @override + def NFet(*args: Any, **kwargs: Any) -> "SwitchFet": + return SwitchFet(*args, **kwargs, channel="N") + @staticmethod + @override + def PFet(*args: Any, **kwargs: Any) -> "SwitchFet": + return SwitchFet(*args, **kwargs, channel="P") - def __init__(self, *, frequency: RangeLike = 0*Hertz(tol=0), drive_current: RangeLike = Range.all(), **kwargs: Any) -> None: - super().__init__(**kwargs) + def __init__( + self, *, frequency: RangeLike = 0 * Hertz(tol=0), drive_current: RangeLike = Range.all(), **kwargs: Any + ) -> None: + super().__init__(**kwargs) - self.frequency = self.ArgParameter(frequency) - self.drive_current = self.ArgParameter(drive_current) # positive is turn-on drive, negative is turn-off drive + self.frequency = self.ArgParameter(frequency) + self.drive_current = self.ArgParameter(drive_current) # positive is turn-on drive, negative is turn-off drive @non_library class TableSwitchFet(PartsTableSelector, SwitchFet, BaseTableFet): - SWITCHING_POWER = PartsTableColumn(Range) - STATIC_POWER = PartsTableColumn(Range) - TOTAL_POWER = PartsTableColumn(Range) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.frequency, self.drain_voltage, self.drain_current, - self.gate_voltage, self.gate_threshold_voltage, - self.rds_on, self.gate_charge, self.power, self.channel, self.drive_current) - - self.actual_static_power = self.Parameter(RangeExpr()) - self.actual_switching_power = self.Parameter(RangeExpr()) - self.actual_total_power = self.Parameter(RangeExpr()) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: # here this is just a pre-filter step - return super()._row_filter(row) and \ - row[self.CHANNEL] == self.get(self.channel) and \ - self.get(self.drain_voltage).fuzzy_in(row[self.VDS_RATING]) and \ - self.get(self.drain_current).fuzzy_in(row[self.IDS_RATING]) and \ - self.get(self.gate_voltage).fuzzy_in(row[self.VGS_RATING]) and \ - (row[self.VGS_DRIVE].lower in self.get(self.gate_threshold_voltage)) and \ - row[self.RDS_ON].fuzzy_in(self.get(self.rds_on)) and \ - row[self.GATE_CHARGE].fuzzy_in(self.get(self.gate_charge)) and \ - self.get(self.power).fuzzy_in(row[self.POWER_RATING]) - - @override - def _table_postprocess(self, table: PartsTable) -> PartsTable: - drive_current = self.get(self.drive_current) - gate_drive_rise, gate_drive_fall = drive_current.upper, -drive_current.lower - assert gate_drive_rise > 0 and gate_drive_fall > 0, \ - f"got nonpositive gate currents rise={gate_drive_rise} A and fall={gate_drive_fall} A" - - drain_current = self.get(self.drain_current) - drain_voltage = self.get(self.drain_voltage) - frequency = self.get(self.frequency) - - def process_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - new_cols: Dict[PartsTableColumn, Any] = {} - new_cols[self.STATIC_POWER] = drain_current * drain_current * row[self.RDS_ON] - - rise_time = row[self.GATE_CHARGE] / gate_drive_rise - fall_time = row[self.GATE_CHARGE] / gate_drive_fall - new_cols[self.SWITCHING_POWER] = (rise_time + fall_time) * (drain_current * drain_voltage) * frequency - - new_cols[self.TOTAL_POWER] = new_cols[self.STATIC_POWER] + new_cols[self.SWITCHING_POWER] - - if new_cols[self.TOTAL_POWER].fuzzy_in(row[self.POWER_RATING]): - return new_cols - else: - return None - - return super()._table_postprocess(table).map_new_columns(process_row) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_drain_voltage_rating, row[self.VDS_RATING]) - self.assign(self.actual_drain_current_rating, row[self.IDS_RATING]) - self.assign(self.actual_gate_voltage_rating, row[self.VGS_RATING]) - self.assign(self.actual_gate_drive, row[self.VGS_DRIVE]) - self.assign(self.actual_rds_on, row[self.RDS_ON]) - self.assign(self.actual_power_rating, row[self.POWER_RATING]) - self.assign(self.actual_gate_charge, row[self.GATE_CHARGE]) - - self.assign(self.actual_static_power, row[self.STATIC_POWER]) - self.assign(self.actual_switching_power, row[self.SWITCHING_POWER]) - self.assign(self.actual_total_power, row[self.TOTAL_POWER]) + SWITCHING_POWER = PartsTableColumn(Range) + STATIC_POWER = PartsTableColumn(Range) + TOTAL_POWER = PartsTableColumn(Range) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param( + self.frequency, + self.drain_voltage, + self.drain_current, + self.gate_voltage, + self.gate_threshold_voltage, + self.rds_on, + self.gate_charge, + self.power, + self.channel, + self.drive_current, + ) + + self.actual_static_power = self.Parameter(RangeExpr()) + self.actual_switching_power = self.Parameter(RangeExpr()) + self.actual_total_power = self.Parameter(RangeExpr()) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: # here this is just a pre-filter step + return ( + super()._row_filter(row) + and row[self.CHANNEL] == self.get(self.channel) + and self.get(self.drain_voltage).fuzzy_in(row[self.VDS_RATING]) + and self.get(self.drain_current).fuzzy_in(row[self.IDS_RATING]) + and self.get(self.gate_voltage).fuzzy_in(row[self.VGS_RATING]) + and (row[self.VGS_DRIVE].lower in self.get(self.gate_threshold_voltage)) + and row[self.RDS_ON].fuzzy_in(self.get(self.rds_on)) + and row[self.GATE_CHARGE].fuzzy_in(self.get(self.gate_charge)) + and self.get(self.power).fuzzy_in(row[self.POWER_RATING]) + ) + + @override + def _table_postprocess(self, table: PartsTable) -> PartsTable: + drive_current = self.get(self.drive_current) + gate_drive_rise, gate_drive_fall = drive_current.upper, -drive_current.lower + assert ( + gate_drive_rise > 0 and gate_drive_fall > 0 + ), f"got nonpositive gate currents rise={gate_drive_rise} A and fall={gate_drive_fall} A" + + drain_current = self.get(self.drain_current) + drain_voltage = self.get(self.drain_voltage) + frequency = self.get(self.frequency) + + def process_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + new_cols: Dict[PartsTableColumn, Any] = {} + new_cols[self.STATIC_POWER] = drain_current * drain_current * row[self.RDS_ON] + + rise_time = row[self.GATE_CHARGE] / gate_drive_rise + fall_time = row[self.GATE_CHARGE] / gate_drive_fall + new_cols[self.SWITCHING_POWER] = (rise_time + fall_time) * (drain_current * drain_voltage) * frequency + + new_cols[self.TOTAL_POWER] = new_cols[self.STATIC_POWER] + new_cols[self.SWITCHING_POWER] + + if new_cols[self.TOTAL_POWER].fuzzy_in(row[self.POWER_RATING]): + return new_cols + else: + return None + + return super()._table_postprocess(table).map_new_columns(process_row) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_drain_voltage_rating, row[self.VDS_RATING]) + self.assign(self.actual_drain_current_rating, row[self.IDS_RATING]) + self.assign(self.actual_gate_voltage_rating, row[self.VGS_RATING]) + self.assign(self.actual_gate_drive, row[self.VGS_DRIVE]) + self.assign(self.actual_rds_on, row[self.RDS_ON]) + self.assign(self.actual_power_rating, row[self.POWER_RATING]) + self.assign(self.actual_gate_charge, row[self.GATE_CHARGE]) + + self.assign(self.actual_static_power, row[self.STATIC_POWER]) + self.assign(self.actual_switching_power, row[self.SWITCHING_POWER]) + self.assign(self.actual_total_power, row[self.TOTAL_POWER]) diff --git a/edg/abstract_parts/AbstractFuse.py b/edg/abstract_parts/AbstractFuse.py index 644c54823..352bcae2e 100644 --- a/edg/abstract_parts/AbstractFuse.py +++ b/edg/abstract_parts/AbstractFuse.py @@ -12,129 +12,151 @@ @abstract_block class Fuse(DiscreteComponent, HasStandardFootprint): - _STANDARD_FOOTPRINT = lambda: FuseStandardFootprint - - def __init__(self, trip_current: RangeLike, *, hold_current: RangeLike = RangeExpr.ALL, - voltage: RangeLike = RangeExpr.ZERO) -> None: - """Model-wise, equivalent to a VoltageSource|Sink passthrough, with a trip rating.""" - super().__init__() - - self.a = self.Port(Passive()) - self.b = self.Port(Passive()) - - self.trip_current = self.ArgParameter(trip_current) # current at which this will trip - self.actual_trip_current = self.Parameter(RangeExpr()) - self.hold_current = self.ArgParameter(hold_current) # current within at which this will NOT trip - self.actual_hold_current = self.Parameter(RangeExpr()) - self.voltage = self.ArgParameter(voltage) # operating voltage - self.actual_voltage_rating = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "trip current: ", DescriptionString.FormatUnits(self.actual_trip_current, "A"), - " of spec: ", DescriptionString.FormatUnits(self.trip_current, "A"), "\n", - "hold current: ", DescriptionString.FormatUnits(self.actual_hold_current, "A"), - " of spec: ", DescriptionString.FormatUnits(self.hold_current, "A"), "\n", - "voltage rating: ", DescriptionString.FormatUnits(self.actual_voltage_rating, "V"), - " of operating: ", DescriptionString.FormatUnits(self.voltage, "V") - ) - - self.require(self.actual_trip_current.within(self.trip_current), - "trip current not within specified rating") - self.require(self.actual_hold_current.within(self.hold_current), - "hold current not within specified rating") - self.require(self.voltage.within(self.actual_voltage_rating), - "operating voltage not within rating") + _STANDARD_FOOTPRINT = lambda: FuseStandardFootprint + + def __init__( + self, trip_current: RangeLike, *, hold_current: RangeLike = RangeExpr.ALL, voltage: RangeLike = RangeExpr.ZERO + ) -> None: + """Model-wise, equivalent to a VoltageSource|Sink passthrough, with a trip rating.""" + super().__init__() + + self.a = self.Port(Passive()) + self.b = self.Port(Passive()) + + self.trip_current = self.ArgParameter(trip_current) # current at which this will trip + self.actual_trip_current = self.Parameter(RangeExpr()) + self.hold_current = self.ArgParameter(hold_current) # current within at which this will NOT trip + self.actual_hold_current = self.Parameter(RangeExpr()) + self.voltage = self.ArgParameter(voltage) # operating voltage + self.actual_voltage_rating = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "trip current: ", + DescriptionString.FormatUnits(self.actual_trip_current, "A"), + " of spec: ", + DescriptionString.FormatUnits(self.trip_current, "A"), + "\n", + "hold current: ", + DescriptionString.FormatUnits(self.actual_hold_current, "A"), + " of spec: ", + DescriptionString.FormatUnits(self.hold_current, "A"), + "\n", + "voltage rating: ", + DescriptionString.FormatUnits(self.actual_voltage_rating, "V"), + " of operating: ", + DescriptionString.FormatUnits(self.voltage, "V"), + ) + + self.require(self.actual_trip_current.within(self.trip_current), "trip current not within specified rating") + self.require(self.actual_hold_current.within(self.hold_current), "hold current not within specified rating") + self.require(self.voltage.within(self.actual_voltage_rating), "operating voltage not within rating") class FuseStandardFootprint(StandardFootprint[Fuse]): - REFDES_PREFIX = 'F' - - FOOTPRINT_PINNING_MAP = { - ( - 'Resistor_SMD:R_0201_0603Metric', - 'Resistor_SMD:R_0402_1005Metric', - 'Resistor_SMD:R_0603_1608Metric', - 'Resistor_SMD:R_0805_2012Metric', - 'Resistor_SMD:R_1206_3216Metric', - 'Resistor_SMD:R_1210_3225Metric', - 'Resistor_SMD:R_1812_4532Metric', - 'Resistor_SMD:R_2010_5025Metric', - 'Resistor_SMD:R_2512_6332Metric', - ): lambda block: { - '1': block.a, - '2': block.b, - }, - } + REFDES_PREFIX = "F" + + FOOTPRINT_PINNING_MAP = { + ( + "Resistor_SMD:R_0201_0603Metric", + "Resistor_SMD:R_0402_1005Metric", + "Resistor_SMD:R_0603_1608Metric", + "Resistor_SMD:R_0805_2012Metric", + "Resistor_SMD:R_1206_3216Metric", + "Resistor_SMD:R_1210_3225Metric", + "Resistor_SMD:R_1812_4532Metric", + "Resistor_SMD:R_2010_5025Metric", + "Resistor_SMD:R_2512_6332Metric", + ): lambda block: { + "1": block.a, + "2": block.b, + }, + } class SeriesPowerFuse(Protection): - """Series fuse for power applications""" - FUSE_TYPE = Fuse - - def __init__(self, trip_current: RangeLike) -> None: - super().__init__() - - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration - self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration - - self.fuse = self.Block(self.FUSE_TYPE( - trip_current=trip_current, - hold_current=(self.pwr_out.link().current_drawn.upper(), float('inf')), - voltage=self.pwr_in.link().voltage - )) - self.connect(self.pwr_in, self.fuse.a.adapt_to(VoltageSink( - voltage_limits=self.fuse.actual_voltage_rating, # TODO: eventually needs a ground ref - current_draw=self.pwr_out.link().current_drawn - ))) - self.connect(self.pwr_out, self.fuse.b.adapt_to(VoltageSource( - voltage_out=self.pwr_in.link().voltage, # ignore voltage drop - current_limits=(0, self.fuse.actual_hold_current.lower()) - ))) - - def connected(self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None) -> \ - 'SeriesPowerFuse': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if pwr_in is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) - if pwr_out is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) - return self + """Series fuse for power applications""" + + FUSE_TYPE = Fuse + + def __init__(self, trip_current: RangeLike) -> None: + super().__init__() + + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration + self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration + + self.fuse = self.Block( + self.FUSE_TYPE( + trip_current=trip_current, + hold_current=(self.pwr_out.link().current_drawn.upper(), float("inf")), + voltage=self.pwr_in.link().voltage, + ) + ) + self.connect( + self.pwr_in, + self.fuse.a.adapt_to( + VoltageSink( + voltage_limits=self.fuse.actual_voltage_rating, # TODO: eventually needs a ground ref + current_draw=self.pwr_out.link().current_drawn, + ) + ), + ) + self.connect( + self.pwr_out, + self.fuse.b.adapt_to( + VoltageSource( + voltage_out=self.pwr_in.link().voltage, # ignore voltage drop + current_limits=(0, self.fuse.actual_hold_current.lower()), + ) + ), + ) + + def connected( + self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None + ) -> "SeriesPowerFuse": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if pwr_in is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) + if pwr_out is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) + return self @abstract_block class PptcFuse(Fuse): - """PPTC self-resetting fuse""" + """PPTC self-resetting fuse""" @non_library class TableFuse(PartsTableSelector, Fuse): - TRIP_CURRENT = PartsTableColumn(Range) - HOLD_CURRENT = PartsTableColumn(Range) - VOLTAGE_RATING = PartsTableColumn(Range) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.trip_current, self.hold_current, self.voltage) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - row[self.TRIP_CURRENT].fuzzy_in(self.get(self.trip_current)) and \ - row[self.HOLD_CURRENT].fuzzy_in(self.get(self.hold_current)) and \ - self.get(self.voltage).fuzzy_in(row[self.VOLTAGE_RATING]) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_trip_current, row[self.TRIP_CURRENT]) - self.assign(self.actual_hold_current, row[self.HOLD_CURRENT]) - self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) + TRIP_CURRENT = PartsTableColumn(Range) + HOLD_CURRENT = PartsTableColumn(Range) + VOLTAGE_RATING = PartsTableColumn(Range) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.trip_current, self.hold_current, self.voltage) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return ( + super()._row_filter(row) + and row[self.TRIP_CURRENT].fuzzy_in(self.get(self.trip_current)) + and row[self.HOLD_CURRENT].fuzzy_in(self.get(self.hold_current)) + and self.get(self.voltage).fuzzy_in(row[self.VOLTAGE_RATING]) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_trip_current, row[self.TRIP_CURRENT]) + self.assign(self.actual_hold_current, row[self.HOLD_CURRENT]) + self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) @deprecated("Use SeriesPowerFuse and a top-level refinement to specify a PPTC fuse") class SeriesPowerPptcFuse(SeriesPowerFuse): - FUSE_TYPE = PptcFuse + FUSE_TYPE = PptcFuse diff --git a/edg/abstract_parts/AbstractInductor.py b/edg/abstract_parts/AbstractInductor.py index b4b2c70f9..272023a07 100644 --- a/edg/abstract_parts/AbstractInductor.py +++ b/edg/abstract_parts/AbstractInductor.py @@ -11,193 +11,208 @@ @abstract_block class Inductor(PassiveComponent, KiCadImportableBlock, HasStandardFootprint): - _STANDARD_FOOTPRINT = lambda: InductorStandardFootprint - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name in ('Device:L', 'Device:L_Small') - return {'1': self.a, '2': self.b} - - def __init__(self, inductance: RangeLike, - current: RangeLike = RangeExpr.ZERO, - frequency: RangeLike = RangeExpr.ZERO, - resistance_dc: RangeLike = (0, 1)*Ohm, # generic sane choice? - *, - experimental_filter_fn: StringLike = "" - ) -> None: - super().__init__() - - self.a = self.Port(Passive.empty()) - self.b = self.Port(Passive.empty()) - - self.inductance = self.ArgParameter(inductance) - self.current = self.ArgParameter(current) # defined as operating current range, non-directioned - self.frequency = self.ArgParameter(frequency) # defined as operating frequency range - self.resistance_dc = self.ArgParameter(resistance_dc) - self.experimental_filter_fn = self.ArgParameter(experimental_filter_fn) - - self.actual_inductance = self.Parameter(RangeExpr()) - self.actual_current_rating = self.Parameter(RangeExpr()) - self.actual_frequency_rating = self.Parameter(RangeExpr()) - self.actual_resistance_dc = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "inductance: ", DescriptionString.FormatUnits(self.actual_inductance, "H"), - " of spec: ", DescriptionString.FormatUnits(self.inductance, "H"), "\n", - "current rating: ", DescriptionString.FormatUnits(self.actual_current_rating, "A"), - " of operating: ", DescriptionString.FormatUnits(self.current, "A"), "\n", - "frequency rating: ", DescriptionString.FormatUnits(self.actual_frequency_rating, "Hz"), - " of operating: ", DescriptionString.FormatUnits(self.frequency, "Hz"), "\n", - "dc resistance: ", DescriptionString.FormatUnits(self.actual_resistance_dc, "Ω"), - " of spec: ", DescriptionString.FormatUnits(self.resistance_dc, "Ω"), - ) + _STANDARD_FOOTPRINT = lambda: InductorStandardFootprint + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name in ("Device:L", "Device:L_Small") + return {"1": self.a, "2": self.b} + + def __init__( + self, + inductance: RangeLike, + current: RangeLike = RangeExpr.ZERO, + frequency: RangeLike = RangeExpr.ZERO, + resistance_dc: RangeLike = (0, 1) * Ohm, # generic sane choice? + *, + experimental_filter_fn: StringLike = "", + ) -> None: + super().__init__() + + self.a = self.Port(Passive.empty()) + self.b = self.Port(Passive.empty()) + + self.inductance = self.ArgParameter(inductance) + self.current = self.ArgParameter(current) # defined as operating current range, non-directioned + self.frequency = self.ArgParameter(frequency) # defined as operating frequency range + self.resistance_dc = self.ArgParameter(resistance_dc) + self.experimental_filter_fn = self.ArgParameter(experimental_filter_fn) + + self.actual_inductance = self.Parameter(RangeExpr()) + self.actual_current_rating = self.Parameter(RangeExpr()) + self.actual_frequency_rating = self.Parameter(RangeExpr()) + self.actual_resistance_dc = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "inductance: ", + DescriptionString.FormatUnits(self.actual_inductance, "H"), + " of spec: ", + DescriptionString.FormatUnits(self.inductance, "H"), + "\n", + "current rating: ", + DescriptionString.FormatUnits(self.actual_current_rating, "A"), + " of operating: ", + DescriptionString.FormatUnits(self.current, "A"), + "\n", + "frequency rating: ", + DescriptionString.FormatUnits(self.actual_frequency_rating, "Hz"), + " of operating: ", + DescriptionString.FormatUnits(self.frequency, "Hz"), + "\n", + "dc resistance: ", + DescriptionString.FormatUnits(self.actual_resistance_dc, "Ω"), + " of spec: ", + DescriptionString.FormatUnits(self.resistance_dc, "Ω"), + ) class InductorStandardFootprint(StandardFootprint[Inductor]): - REFDES_PREFIX = 'L' - - FOOTPRINT_PINNING_MAP = { - ( - 'Inductor_SMD:L_0201_0603Metric', - 'Inductor_SMD:L_0402_1005Metric', - 'Inductor_SMD:L_0603_1608Metric', - 'Inductor_SMD:L_0805_2012Metric', - 'Inductor_SMD:L_1206_3216Metric', - 'Inductor_SMD:L_1210_3225Metric', - 'Inductor_SMD:L_1812_4532Metric', - 'Inductor_SMD:L_2010_5025Metric', - 'Inductor_SMD:L_2512_6332Metric', - - 'Inductor_SMD:L_Bourns-SRR1005', - 'Inductor_SMD:L_Bourns_SRR1210A', - 'Inductor_SMD:L_Bourns_SRR1260', - 'Inductor_SMD:L_Bourns_SRP1245A', - - 'Inductor_SMD:L_Sunlord_SWPA3010S', - 'Inductor_SMD:L_Sunlord_SWPA3012S', - 'Inductor_SMD:L_Sunlord_SWPA3015S', - 'Inductor_SMD:L_Sunlord_SWPA4010S', - 'Inductor_SMD:L_Sunlord_SWPA4012S', - 'Inductor_SMD:L_Sunlord_SWPA4018S', - 'Inductor_SMD:L_Sunlord_SWPA4020S', - 'Inductor_SMD:L_Sunlord_SWPA4026S', - 'Inductor_SMD:L_Sunlord_SWPA4030S', - 'Inductor_SMD:L_Sunlord_SWPA5012S', - 'Inductor_SMD:L_Sunlord_SWPA5020S', - 'Inductor_SMD:L_Sunlord_SWPA5040S', - 'Inductor_SMD:L_Sunlord_SWPA6020S', - 'Inductor_SMD:L_Sunlord_SWPA6028S', - 'Inductor_SMD:L_Sunlord_SWPA6040S', - 'Inductor_SMD:L_Sunlord_SWPA6045S', - 'Inductor_SMD:L_Sunlord_SWPA8040S', - 'Inductor_SMD:L_Sunlord_SWRB1204S', - 'Inductor_SMD:L_Sunlord_SWRB1205S', - 'Inductor_SMD:L_Sunlord_SWRB1207S', - - 'Inductor_SMD:L_Taiyo-Yuden_NR-20xx', - 'Inductor_SMD:L_Taiyo-Yuden_NR-24xx', - 'Inductor_SMD:L_Taiyo-Yuden_NR-30xx', - 'Inductor_SMD:L_Taiyo-Yuden_NR-40xx', - 'Inductor_SMD:L_Taiyo-Yuden_NR-50xx', - 'Inductor_SMD:L_Taiyo-Yuden_NR-60xx', - 'Inductor_SMD:L_Taiyo-Yuden_NR-80xx', - - 'Inductor_SMD:L_TDK_SLF6025', - 'Inductor_SMD:L_TDK_SLF6028', - 'Inductor_SMD:L_TDK_SLF6045', - 'Inductor_SMD:L_TDK_SLF7032', - 'Inductor_SMD:L_TDK_SLF7045', - 'Inductor_SMD:L_TDK_SLF7055', - 'Inductor_SMD:L_TDK_SLF10145', - 'Inductor_SMD:L_TDK_SLF10165', - 'Inductor_SMD:L_TDK_SLF12555', - 'Inductor_SMD:L_TDK_SLF12565', - 'Inductor_SMD:L_TDK_SLF12575', - - 'Inductor_SMD:L_Vishay_IHLP-1212', - 'Inductor_SMD:L_Vishay_IHLP-1616', - 'Inductor_SMD:L_Vishay_IHLP-2020', - 'Inductor_SMD:L_Vishay_IHLP-2525', - 'Inductor_SMD:L_Vishay_IHLP-4040', - 'Inductor_SMD:L_Vishay_IHLP-5050', - 'Inductor_SMD:L_Vishay_IHLP-6767', - ): lambda block: { - '1': block.a, - '2': block.b, - }, - } + REFDES_PREFIX = "L" + + FOOTPRINT_PINNING_MAP = { + ( + "Inductor_SMD:L_0201_0603Metric", + "Inductor_SMD:L_0402_1005Metric", + "Inductor_SMD:L_0603_1608Metric", + "Inductor_SMD:L_0805_2012Metric", + "Inductor_SMD:L_1206_3216Metric", + "Inductor_SMD:L_1210_3225Metric", + "Inductor_SMD:L_1812_4532Metric", + "Inductor_SMD:L_2010_5025Metric", + "Inductor_SMD:L_2512_6332Metric", + "Inductor_SMD:L_Bourns-SRR1005", + "Inductor_SMD:L_Bourns_SRR1210A", + "Inductor_SMD:L_Bourns_SRR1260", + "Inductor_SMD:L_Bourns_SRP1245A", + "Inductor_SMD:L_Sunlord_SWPA3010S", + "Inductor_SMD:L_Sunlord_SWPA3012S", + "Inductor_SMD:L_Sunlord_SWPA3015S", + "Inductor_SMD:L_Sunlord_SWPA4010S", + "Inductor_SMD:L_Sunlord_SWPA4012S", + "Inductor_SMD:L_Sunlord_SWPA4018S", + "Inductor_SMD:L_Sunlord_SWPA4020S", + "Inductor_SMD:L_Sunlord_SWPA4026S", + "Inductor_SMD:L_Sunlord_SWPA4030S", + "Inductor_SMD:L_Sunlord_SWPA5012S", + "Inductor_SMD:L_Sunlord_SWPA5020S", + "Inductor_SMD:L_Sunlord_SWPA5040S", + "Inductor_SMD:L_Sunlord_SWPA6020S", + "Inductor_SMD:L_Sunlord_SWPA6028S", + "Inductor_SMD:L_Sunlord_SWPA6040S", + "Inductor_SMD:L_Sunlord_SWPA6045S", + "Inductor_SMD:L_Sunlord_SWPA8040S", + "Inductor_SMD:L_Sunlord_SWRB1204S", + "Inductor_SMD:L_Sunlord_SWRB1205S", + "Inductor_SMD:L_Sunlord_SWRB1207S", + "Inductor_SMD:L_Taiyo-Yuden_NR-20xx", + "Inductor_SMD:L_Taiyo-Yuden_NR-24xx", + "Inductor_SMD:L_Taiyo-Yuden_NR-30xx", + "Inductor_SMD:L_Taiyo-Yuden_NR-40xx", + "Inductor_SMD:L_Taiyo-Yuden_NR-50xx", + "Inductor_SMD:L_Taiyo-Yuden_NR-60xx", + "Inductor_SMD:L_Taiyo-Yuden_NR-80xx", + "Inductor_SMD:L_TDK_SLF6025", + "Inductor_SMD:L_TDK_SLF6028", + "Inductor_SMD:L_TDK_SLF6045", + "Inductor_SMD:L_TDK_SLF7032", + "Inductor_SMD:L_TDK_SLF7045", + "Inductor_SMD:L_TDK_SLF7055", + "Inductor_SMD:L_TDK_SLF10145", + "Inductor_SMD:L_TDK_SLF10165", + "Inductor_SMD:L_TDK_SLF12555", + "Inductor_SMD:L_TDK_SLF12565", + "Inductor_SMD:L_TDK_SLF12575", + "Inductor_SMD:L_Vishay_IHLP-1212", + "Inductor_SMD:L_Vishay_IHLP-1616", + "Inductor_SMD:L_Vishay_IHLP-2020", + "Inductor_SMD:L_Vishay_IHLP-2525", + "Inductor_SMD:L_Vishay_IHLP-4040", + "Inductor_SMD:L_Vishay_IHLP-5050", + "Inductor_SMD:L_Vishay_IHLP-6767", + ): lambda block: { + "1": block.a, + "2": block.b, + }, + } @non_library class TableInductor(PartsTableSelector, Inductor): - INDUCTANCE = PartsTableColumn(Range) # actual inductance incl. tolerance - FREQUENCY_RATING = PartsTableColumn(Range) # tolerable frequencies - CURRENT_RATING = PartsTableColumn(Range) # tolerable current - DC_RESISTANCE = PartsTableColumn(Range) # actual DCR - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.inductance, self.current, self.frequency, self.resistance_dc, - self.experimental_filter_fn) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - filter_fn_str = self.get(self.experimental_filter_fn) - if filter_fn_str: - filter_fn = ExperimentalUserFnPartsTable.deserialize_fn(filter_fn_str) - else: - filter_fn = None - - return super()._row_filter(row) and \ - row[self.INDUCTANCE].fuzzy_in(self.get(self.inductance)) and \ - self.get(self.current).fuzzy_in(row[self.CURRENT_RATING]) and \ - row[self.DC_RESISTANCE].fuzzy_in(self.get(self.resistance_dc)) and \ - self.get(self.frequency).fuzzy_in(row[self.FREQUENCY_RATING]) and\ - (filter_fn is None or filter_fn(row)) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_inductance, row[self.INDUCTANCE]) - self.assign(self.actual_current_rating, row[self.CURRENT_RATING]) - self.assign(self.actual_frequency_rating, row[self.FREQUENCY_RATING]) - self.assign(self.actual_resistance_dc, row[self.DC_RESISTANCE]) - - @classmethod - @override - def _row_sort_by(cls, row: PartsTableRow) -> Any: - return row[cls.DC_RESISTANCE].center() + INDUCTANCE = PartsTableColumn(Range) # actual inductance incl. tolerance + FREQUENCY_RATING = PartsTableColumn(Range) # tolerable frequencies + CURRENT_RATING = PartsTableColumn(Range) # tolerable current + DC_RESISTANCE = PartsTableColumn(Range) # actual DCR + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param( + self.inductance, self.current, self.frequency, self.resistance_dc, self.experimental_filter_fn + ) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + filter_fn_str = self.get(self.experimental_filter_fn) + if filter_fn_str: + filter_fn = ExperimentalUserFnPartsTable.deserialize_fn(filter_fn_str) + else: + filter_fn = None + + return ( + super()._row_filter(row) + and row[self.INDUCTANCE].fuzzy_in(self.get(self.inductance)) + and self.get(self.current).fuzzy_in(row[self.CURRENT_RATING]) + and row[self.DC_RESISTANCE].fuzzy_in(self.get(self.resistance_dc)) + and self.get(self.frequency).fuzzy_in(row[self.FREQUENCY_RATING]) + and (filter_fn is None or filter_fn(row)) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_inductance, row[self.INDUCTANCE]) + self.assign(self.actual_current_rating, row[self.CURRENT_RATING]) + self.assign(self.actual_frequency_rating, row[self.FREQUENCY_RATING]) + self.assign(self.actual_resistance_dc, row[self.DC_RESISTANCE]) + + @classmethod + @override + def _row_sort_by(cls, row: PartsTableRow) -> Any: + return row[cls.DC_RESISTANCE].center() class SeriesPowerInductor(DiscreteApplication): - """VoltageSource/Sink-typed series inductor for power filtering""" - def __init__(self, inductance: RangeLike, current: RangeLike = RangeExpr.ZERO, - frequency: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() - - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration - self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration - - self.ind = self.Block(Inductor( - inductance=inductance, current=current, frequency=frequency - )) - - self.connect(self.pwr_in, self.ind.a.adapt_to(VoltageSink( - current_draw=self.pwr_out.link().current_drawn - ))) - self.connect(self.pwr_out, self.ind.b.adapt_to(VoltageSource( - voltage_out=self.pwr_in.link().voltage, - ))) - - def connected(self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None) -> \ - 'SeriesPowerInductor': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if pwr_in is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) - if pwr_out is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) - return self + """VoltageSource/Sink-typed series inductor for power filtering""" + + def __init__( + self, inductance: RangeLike, current: RangeLike = RangeExpr.ZERO, frequency: RangeLike = RangeExpr.ZERO + ) -> None: + super().__init__() + + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration + self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration + + self.ind = self.Block(Inductor(inductance=inductance, current=current, frequency=frequency)) + + self.connect(self.pwr_in, self.ind.a.adapt_to(VoltageSink(current_draw=self.pwr_out.link().current_drawn))) + self.connect( + self.pwr_out, + self.ind.b.adapt_to( + VoltageSource( + voltage_out=self.pwr_in.link().voltage, + ) + ), + ) + + def connected( + self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None + ) -> "SeriesPowerInductor": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if pwr_in is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) + if pwr_out is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) + return self diff --git a/edg/abstract_parts/AbstractJumper.py b/edg/abstract_parts/AbstractJumper.py index 2b5d02a46..9e50f542b 100644 --- a/edg/abstract_parts/AbstractJumper.py +++ b/edg/abstract_parts/AbstractJumper.py @@ -6,30 +6,33 @@ @abstract_block class Jumper(DiscreteComponent, Block): - """A two-ported passive-typed jumper (a disconnect-able connection), though is treated - as always connected for model purposes. + """A two-ported passive-typed jumper (a disconnect-able connection), though is treated + as always connected for model purposes. - Wrapping blocks can add typed port and parameter propagation semantics.""" - def __init__(self) -> None: - super().__init__() - self.a = self.Port(Passive()) - self.b = self.Port(Passive()) + Wrapping blocks can add typed port and parameter propagation semantics.""" + + def __init__(self) -> None: + super().__init__() + self.a = self.Port(Passive()) + self.b = self.Port(Passive()) class DigitalJumper(TypedJumper, Block): - def __init__(self) -> None: - super().__init__() - self.input = self.Port(DigitalSink.empty(), [Input]) - self.output = self.Port(DigitalSource.empty(), [Output]) - - @override - def contents(self) -> None: - super().contents() - self.device = self.Block(Jumper()) - self.connect(self.input, self.device.a.adapt_to(DigitalSink( - current_draw=self.output.link().current_drawn - ))) - self.connect(self.output, self.device.b.adapt_to(DigitalSource( - voltage_out=self.input.link().voltage, - output_thresholds=self.input.link().output_thresholds - ))) + def __init__(self) -> None: + super().__init__() + self.input = self.Port(DigitalSink.empty(), [Input]) + self.output = self.Port(DigitalSource.empty(), [Output]) + + @override + def contents(self) -> None: + super().contents() + self.device = self.Block(Jumper()) + self.connect(self.input, self.device.a.adapt_to(DigitalSink(current_draw=self.output.link().current_drawn))) + self.connect( + self.output, + self.device.b.adapt_to( + DigitalSource( + voltage_out=self.input.link().voltage, output_thresholds=self.input.link().output_thresholds + ) + ), + ) diff --git a/edg/abstract_parts/AbstractLed.py b/edg/abstract_parts/AbstractLed.py index 758d45510..7e7f1c662 100644 --- a/edg/abstract_parts/AbstractLed.py +++ b/edg/abstract_parts/AbstractLed.py @@ -16,304 +16,365 @@ @abstract_block class Led(DiscreteSemiconductor, HasStandardFootprint): - _STANDARD_FOOTPRINT = lambda: LedStandardFootprint + _STANDARD_FOOTPRINT = lambda: LedStandardFootprint - # Common color definitions - Red: LedColor = "red" - Green: LedColor = "green" - GreenYellow: LedColor = "greenyellow" # more a mellow green - Blue: LedColor = "blue" - Yellow: LedColor = "yellow" - White: LedColor = "white" - Orange: LedColor = "orange" - Any: LedColor = "" + # Common color definitions + Red: LedColor = "red" + Green: LedColor = "green" + GreenYellow: LedColor = "greenyellow" # more a mellow green + Blue: LedColor = "blue" + Yellow: LedColor = "yellow" + White: LedColor = "white" + Orange: LedColor = "orange" + Any: LedColor = "" - def __init__(self, color: LedColorLike = Any): - super().__init__() + def __init__(self, color: LedColorLike = Any): + super().__init__() - self.color = self.ArgParameter(color) - self.actual_color = self.Parameter(StringExpr()) + self.color = self.ArgParameter(color) + self.actual_color = self.Parameter(StringExpr()) - self.a = self.Port(Passive.empty()) - self.k = self.Port(Passive.empty()) + self.a = self.Port(Passive.empty()) + self.k = self.Port(Passive.empty()) class LedStandardFootprint(StandardFootprint[Led]): - REFDES_PREFIX = 'D' + REFDES_PREFIX = "D" - FOOTPRINT_PINNING_MAP = { - ( - 'LED_SMD:LED_0402_1005Metric', - 'LED_SMD:LED_0603_1608Metric', - 'LED_SMD:LED_0805_2012Metric', - 'LED_SMD:LED_1206_3216Metric', - ): lambda block: { - '2': block.a, - '1': block.k, - }, - } + FOOTPRINT_PINNING_MAP = { + ( + "LED_SMD:LED_0402_1005Metric", + "LED_SMD:LED_0603_1608Metric", + "LED_SMD:LED_0805_2012Metric", + "LED_SMD:LED_1206_3216Metric", + ): lambda block: { + "2": block.a, + "1": block.k, + }, + } @non_library class TableLed(PartsTableSelector, Led): - COLOR = PartsTableColumn(str) + COLOR = PartsTableColumn(str) - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.color) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.color) - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - (not self.get(self.color) or row[self.COLOR] == self.get(self.color)) + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return super()._row_filter(row) and (not self.get(self.color) or row[self.COLOR] == self.get(self.color)) - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_color, row[self.COLOR]) + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_color, row[self.COLOR]) @abstract_block class RgbLedCommonAnode(DiscreteSemiconductor): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.a = self.Port(Passive.empty()) - self.k_red = self.Port(Passive.empty()) - self.k_green = self.Port(Passive.empty()) - self.k_blue = self.Port(Passive.empty()) + self.a = self.Port(Passive.empty()) + self.k_red = self.Port(Passive.empty()) + self.k_green = self.Port(Passive.empty()) + self.k_blue = self.Port(Passive.empty()) # TODO should there be some kind of abstract LED class, that works for both high and low side? class IndicatorLed(Light): - """High-side-driven (default, "common cathode") indicator LED""" - def __init__(self, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (1, 10)*mAmp) -> None: - """Controlled LEDs, with provisions for both current source and sink configurations. - signal_in is a constant-voltage digital source, so this must contain some ballast. - This should not contain amplifiers. - TODO: support non single color wavelength (eg, color temperature?) - TODO: support brightness - TODO: separate RawLed class or similar for use with constant-current drivers""" - super().__init__() + """High-side-driven (default, "common cathode") indicator LED""" - self.target_current_draw = self.Parameter(RangeExpr(current_draw)) + def __init__(self, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (1, 10) * mAmp) -> None: + """Controlled LEDs, with provisions for both current source and sink configurations. + signal_in is a constant-voltage digital source, so this must contain some ballast. + This should not contain amplifiers. + TODO: support non single color wavelength (eg, color temperature?) + TODO: support brightness + TODO: separate RawLed class or similar for use with constant-current drivers""" + super().__init__() - self.signal = self.Port(DigitalSink.empty(), [InOut]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.target_current_draw = self.Parameter(RangeExpr(current_draw)) - self.require(self.signal.current_draw.within((0, self.target_current_draw.upper()))) + self.signal = self.Port(DigitalSink.empty(), [InOut]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.package = self.Block(Led(color)) - self.res = self.Block(Resistor( - resistance=(self.signal.link().voltage.upper() / self.target_current_draw.upper(), - self.signal.link().output_thresholds.upper() / self.target_current_draw.lower()))) + self.require(self.signal.current_draw.within((0, self.target_current_draw.upper()))) - self.connect(self.signal, self.package.a.adapt_to(DigitalSink( - current_draw=self.signal.link().voltage / self.res.actual_resistance - ))) + self.package = self.Block(Led(color)) + self.res = self.Block( + Resistor( + resistance=( + self.signal.link().voltage.upper() / self.target_current_draw.upper(), + self.signal.link().output_thresholds.upper() / self.target_current_draw.lower(), + ) + ) + ) - self.connect(self.res.a, self.package.k) - self.connect(self.res.b.adapt_to(Ground()), self.gnd) + self.connect( + self.signal, + self.package.a.adapt_to(DigitalSink(current_draw=self.signal.link().voltage / self.res.actual_resistance)), + ) + + self.connect(self.res.a, self.package.k) + self.connect(self.res.b.adapt_to(Ground()), self.gnd) class IndicatorLedArray(Light, GeneratorBlock): - """An array of IndicatorLed, just a convenience wrapper.""" - def __init__(self, count: IntLike, color: LedColorLike = Led.Any, *, - current_draw: RangeLike = (1, 10) * mAmp): - super().__init__() - self.signals = self.Port(Vector(DigitalSink.empty()), [InOut]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.color = self.ArgParameter(color) - self.current_draw = self.ArgParameter(current_draw) - self.count = self.ArgParameter(count) - self.generator_param(self.count) - - @override - def generate(self) -> None: - super().generate() - self.led = ElementDict[IndicatorLed]() - for led_i in range(self.get(self.count)): - led = self.led[str(led_i)] = self.Block(IndicatorLed(self.color, current_draw=self.current_draw)) - self.connect(self.signals.append_elt(DigitalSink.empty(), str(led_i)), led.signal) - self.connect(led.gnd, self.gnd) + """An array of IndicatorLed, just a convenience wrapper.""" + + def __init__(self, count: IntLike, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (1, 10) * mAmp): + super().__init__() + self.signals = self.Port(Vector(DigitalSink.empty()), [InOut]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.color = self.ArgParameter(color) + self.current_draw = self.ArgParameter(current_draw) + self.count = self.ArgParameter(count) + self.generator_param(self.count) + + @override + def generate(self) -> None: + super().generate() + self.led = ElementDict[IndicatorLed]() + for led_i in range(self.get(self.count)): + led = self.led[str(led_i)] = self.Block(IndicatorLed(self.color, current_draw=self.current_draw)) + self.connect(self.signals.append_elt(DigitalSink.empty(), str(led_i)), led.signal) + self.connect(led.gnd, self.gnd) @abstract_block class IndicatorSinkLed(Light, Block): - """Abstract part for an low-side-driven ("common anode") indicator LED""" - def __init__(self, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (1, 10)*mAmp) -> None: - """Controlled LEDs, with provisions for both current source and sink configurations. - signal_in is a constant-voltage digital source, so this must contain some ballast. - This should not contain amplifiers.""" - super().__init__() + """Abstract part for an low-side-driven ("common anode") indicator LED""" + + def __init__(self, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (1, 10) * mAmp) -> None: + """Controlled LEDs, with provisions for both current source and sink configurations. + signal_in is a constant-voltage digital source, so this must contain some ballast. + This should not contain amplifiers.""" + super().__init__() - self.color = self.ArgParameter(color) - self.current_draw = self.ArgParameter(current_draw) + self.color = self.ArgParameter(color) + self.current_draw = self.ArgParameter(current_draw) - self.signal = self.Port(DigitalSink.empty(), [InOut]) - self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.signal = self.Port(DigitalSink.empty(), [InOut]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) class IndicatorSinkLedResistor(IndicatorSinkLed): - """TODO: should the resistor sided-ness be configurable, eg as a generator? Similar for IndicatorLed""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + """TODO: should the resistor sided-ness be configurable, eg as a generator? Similar for IndicatorLed""" - self.require(self.signal.current_draw.within((-self.current_draw.upper(), 0))) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) - self.package = self.Block(Led(self.color)) - self.res = self.Block(Resistor( - resistance=(self.signal.link().voltage.upper() / self.current_draw.upper(), - self.signal.link().output_thresholds.upper() / self.current_draw.lower()))) + self.require(self.signal.current_draw.within((-self.current_draw.upper(), 0))) - self.connect(self.package.a.adapt_to(VoltageSink( - current_draw=self.signal.link().voltage / self.res.actual_resistance - )), self.pwr) + self.package = self.Block(Led(self.color)) + self.res = self.Block( + Resistor( + resistance=( + self.signal.link().voltage.upper() / self.current_draw.upper(), + self.signal.link().output_thresholds.upper() / self.current_draw.lower(), + ) + ) + ) - self.connect(self.res.a, self.package.k) - self.connect(self.res.b.adapt_to(DigitalSink( - current_draw=-self.signal.link().voltage / self.res.actual_resistance - )), self.signal) + self.connect( + self.package.a.adapt_to(VoltageSink(current_draw=self.signal.link().voltage / self.res.actual_resistance)), + self.pwr, + ) + + self.connect(self.res.a, self.package.k) + self.connect( + self.res.b.adapt_to(DigitalSink(current_draw=-self.signal.link().voltage / self.res.actual_resistance)), + self.signal, + ) class IndicatorSinkLedArray(Light, GeneratorBlock): - """An array of IndicatorSinkLed, just a convenience wrapper.""" - def __init__(self, count: IntLike, color: LedColorLike = Led.Any, *, - current_draw: RangeLike = (1, 10) * mAmp): - super().__init__() - self.signals = self.Port(Vector(DigitalSink.empty()), [InOut]) - self.pwr = self.Port(VoltageSink.empty(), [Power]) - - self.color = self.ArgParameter(color) - self.current_draw = self.ArgParameter(current_draw) - self.count = self.ArgParameter(count) - self.generator_param(self.count) - - @override - def generate(self) -> None: - super().generate() - self.led = ElementDict[IndicatorSinkLed]() - for led_i in range(self.get(self.count)): - led = self.led[str(led_i)] = self.Block(IndicatorSinkLed(self.color, current_draw=self.current_draw)) - self.connect(self.signals.append_elt(DigitalSink.empty(), str(led_i)), led.signal) - self.connect(led.pwr, self.pwr) + """An array of IndicatorSinkLed, just a convenience wrapper.""" + + def __init__(self, count: IntLike, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (1, 10) * mAmp): + super().__init__() + self.signals = self.Port(Vector(DigitalSink.empty()), [InOut]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + + self.color = self.ArgParameter(color) + self.current_draw = self.ArgParameter(current_draw) + self.count = self.ArgParameter(count) + self.generator_param(self.count) + + @override + def generate(self) -> None: + super().generate() + self.led = ElementDict[IndicatorSinkLed]() + for led_i in range(self.get(self.count)): + led = self.led[str(led_i)] = self.Block(IndicatorSinkLed(self.color, current_draw=self.current_draw)) + self.connect(self.signals.append_elt(DigitalSink.empty(), str(led_i)), led.signal) + self.connect(led.pwr, self.pwr) class VoltageIndicatorLed(Light): - """LED connected to a voltage rail as an indicator that there is voltage present""" - def __init__(self, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (1, 10)*mAmp) -> None: - """ - TODO: support non single color wavelength (eg, color temperature?) - TODO: support brightness - TODO: separate RawLed class or similar for use with constant-current drivers""" - super().__init__() + """LED connected to a voltage rail as an indicator that there is voltage present""" + + def __init__(self, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (1, 10) * mAmp) -> None: + """ + TODO: support non single color wavelength (eg, color temperature?) + TODO: support brightness + TODO: separate RawLed class or similar for use with constant-current drivers""" + super().__init__() - self.target_current_draw = self.Parameter(RangeExpr(current_draw)) + self.target_current_draw = self.Parameter(RangeExpr(current_draw)) - self.signal = self.Port(VoltageSink.empty(), [Power, InOut]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.signal = self.Port(VoltageSink.empty(), [Power, InOut]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.require(self.signal.current_draw.within(current_draw)) + self.require(self.signal.current_draw.within(current_draw)) - self.package = self.Block(Led(color)) - self.res = self.Block(Resistor( - resistance=(self.signal.link().voltage.upper() / self.target_current_draw.upper(), - self.signal.link().voltage.lower() / self.target_current_draw.lower()))) + self.package = self.Block(Led(color)) + self.res = self.Block( + Resistor( + resistance=( + self.signal.link().voltage.upper() / self.target_current_draw.upper(), + self.signal.link().voltage.lower() / self.target_current_draw.lower(), + ) + ) + ) - self.connect(self.signal, self.package.a.adapt_to(VoltageSink( - current_draw=self.signal.link().voltage / self.res.actual_resistance - ))) - self.connect(self.res.a, self.package.k) - self.connect(self.res.b.adapt_to(Ground()), self.gnd) + self.connect( + self.signal, + self.package.a.adapt_to(VoltageSink(current_draw=self.signal.link().voltage / self.res.actual_resistance)), + ) + self.connect(self.res.a, self.package.k) + self.connect(self.res.b.adapt_to(Ground()), self.gnd) # TODO should there be some kind of abstract LED class, that works for both CA and CC type LEDs? class IndicatorSinkRgbLed(Light): - """Common anode indicator RGB LED""" - def __init__(self, current_draw: RangeLike = (1, 10)*mAmp) -> None: - """RGB LEDs, with provisions for both common-anode and common-cathode configurations. - This should not contain amplifiers.""" - # TODO: support brightness - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.signals = self.Port(Vector(DigitalSink.empty()), [InOut]) - signal_red = self.signals.append_elt(DigitalSink.empty(), 'red') - signal_green = self.signals.append_elt(DigitalSink.empty(), 'green') - signal_blue = self.signals.append_elt(DigitalSink.empty(), 'blue') - - self.target_current_draw = self.Parameter(RangeExpr(current_draw)) - self.require(signal_red.current_draw.within((-1 * self.target_current_draw.upper(), 0))) - self.require(signal_green.current_draw.within((-1 * self.target_current_draw.upper(), 0))) - self.require(signal_blue.current_draw.within((-1 * self.target_current_draw.upper(), 0))) - self.require(self.pwr.current_draw.within((0, 3 * self.target_current_draw.upper()))) - - self.package = self.Block(RgbLedCommonAnode()) - - self.red_res = self.Block(Resistor( - resistance=(signal_red.link().voltage.upper() / self.target_current_draw.upper(), - signal_red.link().output_thresholds.upper() / self.target_current_draw.lower()))) - self.green_res = self.Block(Resistor( - resistance=(signal_green.link().voltage.upper() / self.target_current_draw.upper(), - signal_green.link().output_thresholds.upper() / self.target_current_draw.lower()))) - self.blue_res = self.Block(Resistor( - resistance=(signal_blue.link().voltage.upper() / self.target_current_draw.upper(), - signal_blue.link().output_thresholds.upper() / self.target_current_draw.lower()))) - - self.connect(self.red_res.a, self.package.k_red) - self.connect(self.green_res.a, self.package.k_green) - self.connect(self.blue_res.a, self.package.k_blue) - self.connect(self.red_res.b.adapt_to(DigitalSink( - current_draw=(-1 * signal_red.link().voltage.upper() / self.red_res.actual_resistance.lower(), 0) - )), signal_red) - self.connect(self.green_res.b.adapt_to(DigitalSink( - current_draw=(-1 * signal_green.link().voltage.upper() / self.green_res.actual_resistance.lower(), 0) - )), signal_green) - self.connect(self.blue_res.b.adapt_to(DigitalSink( - current_draw=(-1 * signal_blue.link().voltage.upper() / self.blue_res.actual_resistance.lower(), 0) - )), signal_blue) - - self.connect(self.pwr, self.package.a.adapt_to(VoltageSink( - current_draw=signal_red.current_draw * -1 + - signal_green.current_draw * -1 + - signal_blue.current_draw * -1 - ))) + """Common anode indicator RGB LED""" + + def __init__(self, current_draw: RangeLike = (1, 10) * mAmp) -> None: + """RGB LEDs, with provisions for both common-anode and common-cathode configurations. + This should not contain amplifiers.""" + # TODO: support brightness + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.signals = self.Port(Vector(DigitalSink.empty()), [InOut]) + signal_red = self.signals.append_elt(DigitalSink.empty(), "red") + signal_green = self.signals.append_elt(DigitalSink.empty(), "green") + signal_blue = self.signals.append_elt(DigitalSink.empty(), "blue") + + self.target_current_draw = self.Parameter(RangeExpr(current_draw)) + self.require(signal_red.current_draw.within((-1 * self.target_current_draw.upper(), 0))) + self.require(signal_green.current_draw.within((-1 * self.target_current_draw.upper(), 0))) + self.require(signal_blue.current_draw.within((-1 * self.target_current_draw.upper(), 0))) + self.require(self.pwr.current_draw.within((0, 3 * self.target_current_draw.upper()))) + + self.package = self.Block(RgbLedCommonAnode()) + + self.red_res = self.Block( + Resistor( + resistance=( + signal_red.link().voltage.upper() / self.target_current_draw.upper(), + signal_red.link().output_thresholds.upper() / self.target_current_draw.lower(), + ) + ) + ) + self.green_res = self.Block( + Resistor( + resistance=( + signal_green.link().voltage.upper() / self.target_current_draw.upper(), + signal_green.link().output_thresholds.upper() / self.target_current_draw.lower(), + ) + ) + ) + self.blue_res = self.Block( + Resistor( + resistance=( + signal_blue.link().voltage.upper() / self.target_current_draw.upper(), + signal_blue.link().output_thresholds.upper() / self.target_current_draw.lower(), + ) + ) + ) + + self.connect(self.red_res.a, self.package.k_red) + self.connect(self.green_res.a, self.package.k_green) + self.connect(self.blue_res.a, self.package.k_blue) + self.connect( + self.red_res.b.adapt_to( + DigitalSink( + current_draw=(-1 * signal_red.link().voltage.upper() / self.red_res.actual_resistance.lower(), 0) + ) + ), + signal_red, + ) + self.connect( + self.green_res.b.adapt_to( + DigitalSink( + current_draw=( + -1 * signal_green.link().voltage.upper() / self.green_res.actual_resistance.lower(), + 0, + ) + ) + ), + signal_green, + ) + self.connect( + self.blue_res.b.adapt_to( + DigitalSink( + current_draw=(-1 * signal_blue.link().voltage.upper() / self.blue_res.actual_resistance.lower(), 0) + ) + ), + signal_blue, + ) + + self.connect( + self.pwr, + self.package.a.adapt_to( + VoltageSink( + current_draw=signal_red.current_draw * -1 + + signal_green.current_draw * -1 + + signal_blue.current_draw * -1 + ) + ), + ) class IndicatorSinkPackedRgbLedElement(IndicatorSinkLed): - def __init__(self) -> None: - super().__init__(current_draw=RangeExpr()) + def __init__(self) -> None: + super().__init__(current_draw=RangeExpr()) class IndicatorSinkPackedRgbLed(MultipackDevice, MultipackBlock): - def __init__(self) -> None: - super().__init__() - - # Optional multipack definition - self.red = self.PackedPart(IndicatorSinkPackedRgbLedElement()) - self.green = self.PackedPart(IndicatorSinkPackedRgbLedElement()) - self.blue = self.PackedPart(IndicatorSinkPackedRgbLedElement()) - - self.red_sig = self.PackedExport(self.red.signal) - self.green_sig = self.PackedExport(self.green.signal) - self.blue_sig = self.PackedExport(self.blue.signal) - self.red_pwr = self.PackedExport(self.red.pwr) - self.green_pwr = self.PackedExport(self.green.pwr) - self.blue_pwr = self.PackedExport(self.blue.pwr) - - self.pwr = self.Block(PackedVoltageSource()) - self.connect(self.pwr.pwr_ins.request('red'), self.red_pwr) - self.connect(self.pwr.pwr_ins.request('green'), self.green_pwr) - self.connect(self.pwr.pwr_ins.request('blue'), self.blue_pwr) - - self.red_current = self.PackedParameter(self.red.current_draw) - self.green_current = self.PackedParameter(self.green.current_draw) - self.blue_current = self.PackedParameter(self.blue.current_draw) - target_current = self.red_current.intersect(self.green_current.intersect(self.blue_current)) - - self.device = self.Block(IndicatorSinkRgbLed(target_current)) - self.connect(self.device.pwr, self.pwr.pwr_out) - self.connect(self.device.signals.request('red'), self.red_sig) - self.connect(self.device.signals.request('green'), self.green_sig) - self.connect(self.device.signals.request('blue'), self.blue_sig) + def __init__(self) -> None: + super().__init__() + + # Optional multipack definition + self.red = self.PackedPart(IndicatorSinkPackedRgbLedElement()) + self.green = self.PackedPart(IndicatorSinkPackedRgbLedElement()) + self.blue = self.PackedPart(IndicatorSinkPackedRgbLedElement()) + + self.red_sig = self.PackedExport(self.red.signal) + self.green_sig = self.PackedExport(self.green.signal) + self.blue_sig = self.PackedExport(self.blue.signal) + self.red_pwr = self.PackedExport(self.red.pwr) + self.green_pwr = self.PackedExport(self.green.pwr) + self.blue_pwr = self.PackedExport(self.blue.pwr) + + self.pwr = self.Block(PackedVoltageSource()) + self.connect(self.pwr.pwr_ins.request("red"), self.red_pwr) + self.connect(self.pwr.pwr_ins.request("green"), self.green_pwr) + self.connect(self.pwr.pwr_ins.request("blue"), self.blue_pwr) + + self.red_current = self.PackedParameter(self.red.current_draw) + self.green_current = self.PackedParameter(self.green.current_draw) + self.blue_current = self.PackedParameter(self.blue.current_draw) + target_current = self.red_current.intersect(self.green_current.intersect(self.blue_current)) + + self.device = self.Block(IndicatorSinkRgbLed(target_current)) + self.connect(self.device.pwr, self.pwr.pwr_out) + self.connect(self.device.signals.request("red"), self.red_sig) + self.connect(self.device.signals.request("green"), self.green_sig) + self.connect(self.device.signals.request("blue"), self.blue_sig) diff --git a/edg/abstract_parts/AbstractLedDriver.py b/edg/abstract_parts/AbstractLedDriver.py index 0e4b97a8c..2bf1a9691 100644 --- a/edg/abstract_parts/AbstractLedDriver.py +++ b/edg/abstract_parts/AbstractLedDriver.py @@ -8,6 +8,7 @@ class LedDriver(PowerConditioner, Interface): """Abstract current-regulated high-power LED driver. LED ports are passive and should be directly connected to the LED (or LED string).""" + def __init__(self, max_current: RangeLike) -> None: super().__init__() @@ -23,13 +24,15 @@ def __init__(self, max_current: RangeLike) -> None: @deprecated("ripple should be an internal parameter") class LedDriverSwitchingConverter(BlockInterfaceMixin[LedDriver]): """LED driver mixin indicating that the LED driver is a switching converter and with a peak-peak ripple limit.""" - def __init__(self, *args: Any, ripple_limit: FloatLike = float('inf'), **kwargs: Any) -> None: + + def __init__(self, *args: Any, ripple_limit: FloatLike = float("inf"), **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.ripple_limit = self.ArgParameter(ripple_limit) class LedDriverPwm(BlockInterfaceMixin[LedDriver]): """LED driver mixin with PWM input for dimming control.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.pwm = self.Port(DigitalSink.empty(), optional=True) diff --git a/edg/abstract_parts/AbstractOpamp.py b/edg/abstract_parts/AbstractOpamp.py index 215a14272..7cb5da14b 100644 --- a/edg/abstract_parts/AbstractOpamp.py +++ b/edg/abstract_parts/AbstractOpamp.py @@ -8,82 +8,90 @@ @abstract_block class Opamp(Analog, KiCadInstantiableBlock, Block): - """Base class for opamps. Parameters need to be more restricted in subclasses. - """ - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name in ('Simulation_SPICE:OPAMP', 'edg_importable:Opamp') - return {'+': self.inp, '-': self.inn, '3': self.out, 'V+': self.pwr, 'V-': self.gnd} + """Base class for opamps. Parameters need to be more restricted in subclasses.""" - @classmethod - @override - def block_from_symbol(cls, symbol_name: str, properties: Mapping[str, str]) -> 'Opamp': - return Opamp() + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name in ("Simulation_SPICE:OPAMP", "edg_importable:Opamp") + return {"+": self.inp, "-": self.inn, "3": self.out, "V+": self.pwr, "V-": self.gnd} - def __init__(self) -> None: - super().__init__() + @classmethod + @override + def block_from_symbol(cls, symbol_name: str, properties: Mapping[str, str]) -> "Opamp": + return Opamp() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) + def __init__(self) -> None: + super().__init__() - self.inp = self.Port(AnalogSink.empty()) - self.inn = self.Port(AnalogSink.empty()) - self.out = self.Port(AnalogSource.empty()) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.inp = self.Port(AnalogSink.empty()) + self.inn = self.Port(AnalogSink.empty()) + self.out = self.Port(AnalogSource.empty()) class OpampElement(Opamp): - """Packed opamp element""" + """Packed opamp element""" @abstract_block class MultipackOpamp(MultipackDevice, MultipackBlock): - """Base class for packed opamps - devices that have multiple opamps in a single package, - with shared power and ground connections. Typically used with the multipack feature to - fit individual opamps across the design hierarchy into one of these.""" - def __init__(self) -> None: - super().__init__() - self.elements = self.PackedPart(PackedBlockArray(OpampElement())) - self.pwr = self.PackedExport(self.elements.ports_array(lambda x: x.pwr)) - self.gnd = self.PackedExport(self.elements.ports_array(lambda x: x.gnd)) - self.inp = self.PackedExport(self.elements.ports_array(lambda x: x.inp)) - self.inn = self.PackedExport(self.elements.ports_array(lambda x: x.inn)) - self.out = self.PackedExport(self.elements.ports_array(lambda x: x.out)) + """Base class for packed opamps - devices that have multiple opamps in a single package, + with shared power and ground connections. Typically used with the multipack feature to + fit individual opamps across the design hierarchy into one of these.""" + + def __init__(self) -> None: + super().__init__() + self.elements = self.PackedPart(PackedBlockArray(OpampElement())) + self.pwr = self.PackedExport(self.elements.ports_array(lambda x: x.pwr)) + self.gnd = self.PackedExport(self.elements.ports_array(lambda x: x.gnd)) + self.inp = self.PackedExport(self.elements.ports_array(lambda x: x.inp)) + self.inn = self.PackedExport(self.elements.ports_array(lambda x: x.inn)) + self.out = self.PackedExport(self.elements.ports_array(lambda x: x.out)) @non_library class MultipackOpampGenerator(MultipackOpamp, GeneratorBlock): - """Skeleton base class that provides scaffolding for common packed opamp definitions""" - class OpampPorts(NamedTuple): - gnd: Ground - pwr: VoltageSink - amps: List[Tuple[AnalogSink, AnalogSink, AnalogSource]] # amp-, amp+, out - - def __init__(self) -> None: - super().__init__() - self.generator_param(self.pwr.requested(), self.gnd.requested(), - self.inn.requested(), self.inp.requested(), self.out.requested()) - - def _make_multipack_opamp(self) -> OpampPorts: - """Generates the opamp as a block in self, including any application circuit components like decoupling capacitors. - Returns (gnd, pwr, [(in-, in+, out)]).""" - raise NotImplementedError # implement me - - @override - def generate(self) -> None: - super().generate() - amp_ports = self._make_multipack_opamp() - - self.gnd_merge = self.Block(PackedGround()) - self.pwr_merge = self.Block(PackedVoltageSource()) - self.connect(self.gnd_merge.gnd_out, amp_ports.gnd) - self.connect(self.pwr_merge.pwr_out, amp_ports.pwr) - - requested = self.get(self.pwr.requested()) - assert self.get(self.gnd.requested()) == self.get(self.inp.requested()) == \ - self.get(self.inn.requested()) == self.get(self.out.requested()) == requested - for i, (amp_neg, amp_pos, amp_out) in zip(requested, amp_ports.amps): - self.connect(self.pwr.append_elt(VoltageSink.empty(), i), self.pwr_merge.pwr_ins.request(i)) - self.connect(self.gnd.append_elt(Ground.empty(), i), self.gnd_merge.gnd_ins.request(i)) - self.connect(self.inn.append_elt(AnalogSink.empty(), i), amp_neg) - self.connect(self.inp.append_elt(AnalogSink.empty(), i), amp_pos) - self.connect(self.out.append_elt(AnalogSource.empty(), i), amp_out) + """Skeleton base class that provides scaffolding for common packed opamp definitions""" + + class OpampPorts(NamedTuple): + gnd: Ground + pwr: VoltageSink + amps: List[Tuple[AnalogSink, AnalogSink, AnalogSource]] # amp-, amp+, out + + def __init__(self) -> None: + super().__init__() + self.generator_param( + self.pwr.requested(), self.gnd.requested(), self.inn.requested(), self.inp.requested(), self.out.requested() + ) + + def _make_multipack_opamp(self) -> OpampPorts: + """Generates the opamp as a block in self, including any application circuit components like decoupling capacitors. + Returns (gnd, pwr, [(in-, in+, out)]).""" + raise NotImplementedError # implement me + + @override + def generate(self) -> None: + super().generate() + amp_ports = self._make_multipack_opamp() + + self.gnd_merge = self.Block(PackedGround()) + self.pwr_merge = self.Block(PackedVoltageSource()) + self.connect(self.gnd_merge.gnd_out, amp_ports.gnd) + self.connect(self.pwr_merge.pwr_out, amp_ports.pwr) + + requested = self.get(self.pwr.requested()) + assert ( + self.get(self.gnd.requested()) + == self.get(self.inp.requested()) + == self.get(self.inn.requested()) + == self.get(self.out.requested()) + == requested + ) + for i, (amp_neg, amp_pos, amp_out) in zip(requested, amp_ports.amps): + self.connect(self.pwr.append_elt(VoltageSink.empty(), i), self.pwr_merge.pwr_ins.request(i)) + self.connect(self.gnd.append_elt(Ground.empty(), i), self.gnd_merge.gnd_ins.request(i)) + self.connect(self.inn.append_elt(AnalogSink.empty(), i), amp_neg) + self.connect(self.inp.append_elt(AnalogSink.empty(), i), amp_pos) + self.connect(self.out.append_elt(AnalogSource.empty(), i), amp_out) diff --git a/edg/abstract_parts/AbstractOscillator.py b/edg/abstract_parts/AbstractOscillator.py index 1654e5c05..5764f6961 100644 --- a/edg/abstract_parts/AbstractOscillator.py +++ b/edg/abstract_parts/AbstractOscillator.py @@ -9,44 +9,47 @@ @abstract_block class Oscillator(DiscreteApplication): - """Device that generates a digital clock signal given power.""" - def __init__(self, frequency: RangeLike) -> None: - super().__init__() + """Device that generates a digital clock signal given power.""" - self.frequency = self.ArgParameter(frequency) - self.actual_frequency = self.Parameter(RangeExpr()) + def __init__(self, frequency: RangeLike) -> None: + super().__init__() - self.gnd = self.Port(Ground.empty(), [Common]) - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.out = self.Port(DigitalSource.empty(), [Output]) + self.frequency = self.ArgParameter(frequency) + self.actual_frequency = self.Parameter(RangeExpr()) - @override - def contents(self) -> None: - super().contents() + self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.out = self.Port(DigitalSource.empty(), [Output]) - self.description = DescriptionString( - "frequency: ", DescriptionString.FormatUnits(self.actual_frequency, "Hz"), - " of spec: ", DescriptionString.FormatUnits(self.frequency, "Hz"), - ) + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "frequency: ", + DescriptionString.FormatUnits(self.actual_frequency, "Hz"), + " of spec: ", + DescriptionString.FormatUnits(self.frequency, "Hz"), + ) @non_library class TableOscillator(PartsTableSelector, Oscillator): - """Provides basic part table matching functionality for oscillators, by frequency only. - Unlike other table-based passive components, this should generate the full application circuit. - No default footprints are provided since these may be non-standard.""" - FREQUENCY = PartsTableColumn(Range) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.frequency) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - row[self.FREQUENCY] in self.get(self.frequency) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_frequency, row[self.FREQUENCY]) + """Provides basic part table matching functionality for oscillators, by frequency only. + Unlike other table-based passive components, this should generate the full application circuit. + No default footprints are provided since these may be non-standard.""" + + FREQUENCY = PartsTableColumn(Range) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.frequency) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return super()._row_filter(row) and row[self.FREQUENCY] in self.get(self.frequency) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_frequency, row[self.FREQUENCY]) diff --git a/edg/abstract_parts/AbstractPowerConverters.py b/edg/abstract_parts/AbstractPowerConverters.py index 5f9fc3803..55d47f021 100644 --- a/edg/abstract_parts/AbstractPowerConverters.py +++ b/edg/abstract_parts/AbstractPowerConverters.py @@ -14,741 +14,935 @@ @abstract_block_default(lambda: IdealVoltageRegulator) class VoltageRegulator(PowerConditioner): - """Structural abstract base class for DC-DC voltage regulators with shared ground (non-isolated). - This takes some input voltage and produces a stable voltage at output_voltage on its output. + """Structural abstract base class for DC-DC voltage regulators with shared ground (non-isolated). + This takes some input voltage and produces a stable voltage at output_voltage on its output. - While this abstract class does not define any limitations on the output voltage, subclasses and concrete - implementations commonly have restrictions, for example linear regulators can only produce voltages lower - than the input voltage. - """ - def __init__(self, output_voltage: RangeLike) -> None: - super().__init__() + While this abstract class does not define any limitations on the output voltage, subclasses and concrete + implementations commonly have restrictions, for example linear regulators can only produce voltages lower + than the input voltage. + """ + + def __init__(self, output_voltage: RangeLike) -> None: + super().__init__() - self.output_voltage = self.ArgParameter(output_voltage) + self.output_voltage = self.ArgParameter(output_voltage) - self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) + self.gnd = self.Port(Ground.empty(), [Common]) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.description = DescriptionString( - "output voltage: ", DescriptionString.FormatUnits(self.pwr_out.voltage_out, "V"), - " of spec: ", DescriptionString.FormatUnits(self.output_voltage, "V"), "\n", - "input voltage: ", DescriptionString.FormatUnits(self.pwr_in.link().voltage, "V") - ) + self.description = DescriptionString( + "output voltage: ", + DescriptionString.FormatUnits(self.pwr_out.voltage_out, "V"), + " of spec: ", + DescriptionString.FormatUnits(self.output_voltage, "V"), + "\n", + "input voltage: ", + DescriptionString.FormatUnits(self.pwr_in.link().voltage, "V"), + ) - self.require(self.pwr_out.voltage_out.within(self.output_voltage), - "Output voltage must be within spec") + self.require(self.pwr_out.voltage_out.within(self.output_voltage), "Output voltage must be within spec") @non_library class VoltageRegulatorEnableWrapper(Resettable, VoltageRegulator, GeneratorBlock): - """Implementation mixin for a voltage regulator wrapper block where the inner device has a reset/enable pin - (active-high enable / active-low shutdown) that is automatically tied high if not externally connected. - Mix this into a VoltageRegulator to automatically handle the reset pin.""" - @abstractmethod - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - """Returns the inner device's reset pin, to be connected in the generator. - Only called within a generator.""" - - @override - def contents(self) -> None: - super().contents() - self.generator_param(self.reset.is_connected()) - - @override - def generate(self) -> None: - super().generate() - if self.get(self.reset.is_connected()): - self.connect(self.reset, self._generator_inner_reset_pin()) - else: # by default tie high to enable regulator - self.connect(self.pwr_in.as_digital_source(), self._generator_inner_reset_pin()) + """Implementation mixin for a voltage regulator wrapper block where the inner device has a reset/enable pin + (active-high enable / active-low shutdown) that is automatically tied high if not externally connected. + Mix this into a VoltageRegulator to automatically handle the reset pin.""" + + @abstractmethod + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + """Returns the inner device's reset pin, to be connected in the generator. + Only called within a generator.""" + + @override + def contents(self) -> None: + super().contents() + self.generator_param(self.reset.is_connected()) + + @override + def generate(self) -> None: + super().generate() + if self.get(self.reset.is_connected()): + self.connect(self.reset, self._generator_inner_reset_pin()) + else: # by default tie high to enable regulator + self.connect(self.pwr_in.as_digital_source(), self._generator_inner_reset_pin()) @abstract_block_default(lambda: IdealLinearRegulator) class LinearRegulator(VoltageRegulator): - """Structural abstract base class for linear regulators, a voltage regulator that can produce some - output voltage lower than its input voltage (minus some dropout) by 'burning' the excess voltage as heat. + """Structural abstract base class for linear regulators, a voltage regulator that can produce some + output voltage lower than its input voltage (minus some dropout) by 'burning' the excess voltage as heat. - Compared to switching converters like buck and boost converters, linear regulators usually have lower - complexity, lower parts count, and higher stability. However, depending on the application, they are - typically less efficient, and at higher loads may require thermal design considerations.""" + Compared to switching converters like buck and boost converters, linear regulators usually have lower + complexity, lower parts count, and higher stability. However, depending on the application, they are + typically less efficient, and at higher loads may require thermal design considerations.""" @abstract_block class VoltageReference(LinearRegulator): - """Voltage reference, generally provides high accuracy but limited current""" + """Voltage reference, generally provides high accuracy but limited current""" class IdealLinearRegulator(Resettable, LinearRegulator, IdealModel): - """Ideal linear regulator, draws the output current and produces spec output voltage limited by input voltage""" - @override - def contents(self) -> None: - super().contents() - effective_output_voltage = self.output_voltage.intersect((0, self.pwr_in.link().voltage.upper())) - self.gnd.init_from(Ground()) - self.pwr_in.init_from(VoltageSink( - current_draw=self.pwr_out.link().current_drawn)) - self.pwr_out.init_from(VoltageSource( - voltage_out=effective_output_voltage)) - self.reset.init_from(DigitalSink()) + """Ideal linear regulator, draws the output current and produces spec output voltage limited by input voltage""" + + @override + def contents(self) -> None: + super().contents() + effective_output_voltage = self.output_voltage.intersect((0, self.pwr_in.link().voltage.upper())) + self.gnd.init_from(Ground()) + self.pwr_in.init_from(VoltageSink(current_draw=self.pwr_out.link().current_drawn)) + self.pwr_out.init_from(VoltageSource(voltage_out=effective_output_voltage)) + self.reset.init_from(DigitalSink()) @non_library class LinearRegulatorDevice(Block): - """Abstract base class that provides a default model with common functionality for a linear regulator chip. - Does not include supporting components like capacitors. - """ - def __init__(self) -> None: - super().__init__() - - # these device model parameters must be provided by subtypes - self.actual_dropout = self.Parameter(RangeExpr()) - self.actual_quiescent_current = self.Parameter(RangeExpr()) - - self.gnd = self.Port(Ground(), [Common]) - self.pwr_in = self.Port(VoltageSink( - voltage_limits=RangeExpr(), # parameters set by subtype - current_draw=RangeExpr() - ), [Power, Input]) - self.pwr_out = self.Port(VoltageSource( - voltage_out=self.RangeExpr(), # parameters set by subtype - current_limits=RangeExpr() - ), [Output]) - self.assign(self.pwr_in.current_draw, - self.pwr_out.link().current_drawn + self.actual_quiescent_current) - - self.require(self.pwr_out.voltage_out.lower() + self.actual_dropout.upper() <= self.pwr_in.link().voltage.lower(), - "excessive dropout") + """Abstract base class that provides a default model with common functionality for a linear regulator chip. + Does not include supporting components like capacitors. + """ + def __init__(self) -> None: + super().__init__() -@abstract_block -class SwitchingVoltageRegulator(VoltageRegulator): - @staticmethod - @deprecated("ripple calculation moved into the power-path block itself") - def _calculate_ripple(output_current: RangeLike, ripple_ratio: RangeLike, *, - rated_current: Optional[FloatLike] = None) -> RangeExpr: - """ - Calculates the target inductor ripple current (with parameters - concrete values not necessary) - given the output current draw, and optionally a non-default ripple ratio and rated current. + # these device model parameters must be provided by subtypes + self.actual_dropout = self.Parameter(RangeExpr()) + self.actual_quiescent_current = self.Parameter(RangeExpr()) - In general, ripple current largely trades off inductor maximum current and inductance. + self.gnd = self.Port(Ground(), [Common]) + self.pwr_in = self.Port( + VoltageSink(voltage_limits=RangeExpr(), current_draw=RangeExpr()), # parameters set by subtype + [Power, Input], + ) + self.pwr_out = self.Port( + VoltageSource(voltage_out=self.RangeExpr(), current_limits=RangeExpr()), # parameters set by subtype + [Output], + ) + self.assign(self.pwr_in.current_draw, self.pwr_out.link().current_drawn + self.actual_quiescent_current) - The default ripple ratio is an expansion of the heuristic 0.3-0.4 to account for tolerancing. - the rated current is used to set a reasonable ceiling for ripple current, when the actual current - is very low. Per the LMR33630 datasheet, the device's rated current should be used in these cases. - """ - output_current_range = RangeExpr._to_expr_type(output_current) - ripple_ratio_range = RangeExpr._to_expr_type(ripple_ratio) - upper_ripple_limit = ripple_ratio_range.upper() * output_current_range.upper() - if rated_current is not None: # if rated current is specified, extend the upper limit for small current draws - # this fallback limit is an arbitrary and low 0.2, not tied to specified ripple ratio since - # it leads to unintuitive behavior where as the low bound increases (range shrinks) the inductance - # spec actually becomes larger - upper_ripple_limit = upper_ripple_limit.max(0.2 * rated_current) - return RangeExpr._to_expr_type(( - ripple_ratio_range.lower() * output_current_range.upper(), - upper_ripple_limit)) - - def __init__(self, *args: Any, - input_ripple_limit: FloatLike = 75 * mVolt, - output_ripple_limit: FloatLike = 25 * mVolt, - **kwargs: Any) -> None: - """https://www.ti.com/lit/an/slta055/slta055.pdf: recommends 75mV for maximum peak-peak ripple voltage - """ - super().__init__(*args, **kwargs) + self.require( + self.pwr_out.voltage_out.lower() + self.actual_dropout.upper() <= self.pwr_in.link().voltage.lower(), + "excessive dropout", + ) - self.input_ripple_limit = self.ArgParameter(input_ripple_limit) - self.output_ripple_limit = self.ArgParameter(output_ripple_limit) - self.actual_frequency = self.Parameter(RangeExpr()) +@abstract_block +class SwitchingVoltageRegulator(VoltageRegulator): + @staticmethod + @deprecated("ripple calculation moved into the power-path block itself") + def _calculate_ripple( + output_current: RangeLike, ripple_ratio: RangeLike, *, rated_current: Optional[FloatLike] = None + ) -> RangeExpr: + """ + Calculates the target inductor ripple current (with parameters - concrete values not necessary) + given the output current draw, and optionally a non-default ripple ratio and rated current. + + In general, ripple current largely trades off inductor maximum current and inductance. + + The default ripple ratio is an expansion of the heuristic 0.3-0.4 to account for tolerancing. + the rated current is used to set a reasonable ceiling for ripple current, when the actual current + is very low. Per the LMR33630 datasheet, the device's rated current should be used in these cases. + """ + output_current_range = RangeExpr._to_expr_type(output_current) + ripple_ratio_range = RangeExpr._to_expr_type(ripple_ratio) + upper_ripple_limit = ripple_ratio_range.upper() * output_current_range.upper() + if rated_current is not None: # if rated current is specified, extend the upper limit for small current draws + # this fallback limit is an arbitrary and low 0.2, not tied to specified ripple ratio since + # it leads to unintuitive behavior where as the low bound increases (range shrinks) the inductance + # spec actually becomes larger + upper_ripple_limit = upper_ripple_limit.max(0.2 * rated_current) + return RangeExpr._to_expr_type((ripple_ratio_range.lower() * output_current_range.upper(), upper_ripple_limit)) + + def __init__( + self, + *args: Any, + input_ripple_limit: FloatLike = 75 * mVolt, + output_ripple_limit: FloatLike = 25 * mVolt, + **kwargs: Any, + ) -> None: + """https://www.ti.com/lit/an/slta055/slta055.pdf: recommends 75mV for maximum peak-peak ripple voltage""" + super().__init__(*args, **kwargs) + + self.input_ripple_limit = self.ArgParameter(input_ripple_limit) + self.output_ripple_limit = self.ArgParameter(output_ripple_limit) + + self.actual_frequency = self.Parameter(RangeExpr()) @abstract_block_default(lambda: IdealBuckConverter) class BuckConverter(SwitchingVoltageRegulator): - """Step-down switching converter""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.require(self.pwr_out.voltage_out.upper() <= self.pwr_in.voltage_limits.upper()) + """Step-down switching converter""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.require(self.pwr_out.voltage_out.upper() <= self.pwr_in.voltage_limits.upper()) @abstract_block_default(lambda: IdealBuckConverter) class DiscreteBuckConverter(BuckConverter): - """Category for discrete buck converter subcircuits (as opposed to integrated components)""" + """Category for discrete buck converter subcircuits (as opposed to integrated components)""" class IdealBuckConverter(Resettable, DiscreteBuckConverter, IdealModel): - """Ideal buck converter producing the spec output voltage (buck-boost) limited by input voltage - and drawing input current from conversation of power""" - @override - def contents(self) -> None: - super().contents() - effective_output_voltage = self.output_voltage.intersect((0, self.pwr_in.link().voltage.upper())) - self.gnd.init_from(Ground()) - self.pwr_in.init_from(VoltageSink( - current_draw=effective_output_voltage / self.pwr_in.link().voltage * self.pwr_out.link().current_drawn)) - self.pwr_out.init_from(VoltageSource( - voltage_out=effective_output_voltage)) - self.reset.init_from(DigitalSink()) + """Ideal buck converter producing the spec output voltage (buck-boost) limited by input voltage + and drawing input current from conversation of power""" + + @override + def contents(self) -> None: + super().contents() + effective_output_voltage = self.output_voltage.intersect((0, self.pwr_in.link().voltage.upper())) + self.gnd.init_from(Ground()) + self.pwr_in.init_from( + VoltageSink( + current_draw=effective_output_voltage / self.pwr_in.link().voltage * self.pwr_out.link().current_drawn + ) + ) + self.pwr_out.init_from(VoltageSource(voltage_out=effective_output_voltage)) + self.reset.init_from(DigitalSink()) class BuckConverterPowerPath(InternalSubcircuit, GeneratorBlock): - """A helper block to generate the power path (inductors, capacitors) for a switching buck converter. - - Useful resources: - https://www.ti.com/lit/an/slva477b/slva477b.pdf - Component sizing in continuous mode - http://www.onmyphd.com/?p=voltage.regulators.buck.step.down.converter - Very detailed analysis including component sizing, operating modes, calculating losses - """ - - @staticmethod - def _d_inverse_d(d_range: Range) -> Range: - """Some power calculations require the maximum of D*(1-D), which has a maximum at D=0.5""" - # can't use range ops since they will double-count the tolerance of D, so calculate endpoints separately - range_endpoints = [d_range.lower * (1 - d_range.lower), d_range.upper * (1 - d_range.upper)] - raw_range = Range(min(range_endpoints), max(range_endpoints)) - if 0.5 in d_range: # the function has a maximum at 0.5 - return raw_range.hull(Range.exact(0.5 * (1 - 0.5))) - else: - return raw_range - - @staticmethod - def _ripple_current_from_sw_current(sw_current: float, ripple_ratio: Range) -> Range: - """Calculates the ripple current from a total switch current and ripple ratio.""" - return Range( # separate range parts to avoid double-counting tolerances - sw_current / (1 + ripple_ratio.lower) * ripple_ratio.lower, - sw_current / (1 + ripple_ratio.upper) * ripple_ratio.upper - ) - - class Values(NamedTuple): - dutycycle: Range - inductance: Range - input_capacitance: Range - output_capacitance: Range - - inductor_avg_current: Range - ripple_scale: float # divide this by inductance to get the inductor ripple current - min_ripple: float # fallback minimum ripple current for component sizing for light-load, may be 0 - output_capacitance_scale: float # multiply inductor ripple by this to get required output capacitance - - inductor_peak_currents: Range # based on the worst case input spec, for unit testing - effective_dutycycle: Range # duty cycle adjusted for tracking behavior - - @classmethod - def _calculate_parameters(cls, input_voltage: Range, output_voltage: Range, frequency: Range, output_current: Range, - sw_current_limits: Range, ripple_ratio: Range, - input_voltage_ripple: float, output_voltage_ripple: float, - efficiency: Range = Range(0.9, 1.0), dutycycle_limit: Range = Range(0.1, 0.9), - limit_ripple_ratio: Range = Range(0.1, 0.5)) -> 'BuckConverterPowerPath.Values': - """Calculates parameters for the buck converter power path. - - This uses the continuous conduction mode (CCM) equations to calculate component sizes. - DCM is not explicitly calculated since it requires additional parameters like minimum on-time. - The limit_ripple_ratio provides some broadly sane values for light-load / DCM operation. - This also ignores higher-order component behavior like capacitor ESR. - - The ripple_ratio is optional and may be set to Range.all(), allowing the inductor selector - to optimize the inductor by trading off inductance and max current. - - Values for component selections are bounded by: - - the ripple_ratio at output_current (if ripple_ratio < inf), and - - the limit_ripple_ratio at sw_current_limits (if sw_current_limits is not zero), as a fallback - for light-load conditions (where otherwise current goes to zero and inductance goes to the moon) + """A helper block to generate the power path (inductors, capacitors) for a switching buck converter. + + Useful resources: + https://www.ti.com/lit/an/slva477b/slva477b.pdf + Component sizing in continuous mode + http://www.onmyphd.com/?p=voltage.regulators.buck.step.down.converter + Very detailed analysis including component sizing, operating modes, calculating losses """ - dutycycle = output_voltage / input_voltage / efficiency - effective_dutycycle = dutycycle.bound_to(dutycycle_limit) # account for tracking behavior - - # calculate minimum inductance based on worst case values (operating range corners producing maximum inductance) - # worst-case input/output voltages and frequency is used to avoid double-counting tolerances as ranges - # note, for buck converter, L = (Vin - Vout) * D / (f * Iripple) = Vout (Vin - Vout) / (Iripple * f * Vin) - # this is at a maximum at Vin,max, and on that curve with a critical point at Vout = Vin,max / 2 - # note, the same formula calculates ripple-from-inductance and inductance-from-ripple - inductance_scale_candidates = [ - output_voltage.lower * (input_voltage.upper - output_voltage.lower) / input_voltage.upper, - output_voltage.upper * (input_voltage.upper - output_voltage.upper) / input_voltage.upper, - ] - if input_voltage.upper / 2 in output_voltage: - inductance_scale_candidates.append( - input_voltage.upper/2 * (input_voltage.upper - input_voltage.upper/2) / input_voltage.upper) - inductance_scale = max(inductance_scale_candidates) / frequency.lower - - inductance = Range.all() - min_ripple = 0.0 - if sw_current_limits.upper > 0: # fallback for light-load - ripple_current = cls._ripple_current_from_sw_current(sw_current_limits.upper, limit_ripple_ratio) - inductance = inductance.intersect(inductance_scale / ripple_current) - min_ripple = ripple_current.lower - if ripple_ratio.upper < float('inf'): - assert ripple_ratio.lower > 0, f"invalid non-inf ripple ratio {ripple_ratio}" - - inductance = inductance.intersect(inductance_scale / (output_current.upper * ripple_ratio)) - assert inductance.upper < float('inf'), 'neither ripple_ratio nor fallback sw_current_limits given' - - input_capacitance = Range.from_lower(output_current.upper * cls._d_inverse_d(effective_dutycycle).upper / - (frequency.lower * input_voltage_ripple)) - output_capacitance_scale = 1 / (8 * frequency.lower * output_voltage_ripple) - - # these are static worst-case estimates for the range of specified ripple currents - # mainly used for unit testing - inductor_current_ripple = output_current * ripple_ratio.intersect(limit_ripple_ratio) - inductor_peak_currents = Range(max(0, output_current.lower - inductor_current_ripple.upper / 2), - max(output_current.upper + inductor_current_ripple.upper / 2, - inductor_current_ripple.upper)) - output_capacitance = Range.from_lower(output_capacitance_scale * inductor_current_ripple.upper) - - return cls.Values(dutycycle=dutycycle, inductance=inductance, - input_capacitance=input_capacitance, output_capacitance=output_capacitance, - inductor_avg_current=output_current / efficiency, - ripple_scale=inductance_scale, min_ripple=min_ripple, - output_capacitance_scale=output_capacitance_scale, - inductor_peak_currents=inductor_peak_currents, - effective_dutycycle=effective_dutycycle) - - @staticmethod - @ExperimentalUserFnPartsTable.user_fn([float, float, float]) - def _buck_inductor_filter(max_avg_current: float, ripple_scale: float, min_ripple: float) -> Callable[[PartsTableRow], bool]: - """Applies further filtering to inductors using the trade-off between inductance and peak-peak current. - max_avg_current is the maximum average current (not accounting for ripple) seen by the inductor - ripple_scale is the scaling factor from 1/L to ripple - This structure also works for boost converters, which would have its ripple_scale calculated differently.""" - def filter_fn(row: PartsTableRow) -> bool: - ripple_current = max(ripple_scale / row[TableInductor.INDUCTANCE].lower, min_ripple) - max_current_pp = max_avg_current + ripple_current / 2 - return max_current_pp in row[TableInductor.CURRENT_RATING] - return filter_fn - - @staticmethod - def _ilim_expr(inductor_ilim: RangeExpr, sw_ilim: RangeExpr, inductor_iripple: RangeExpr) -> RangeExpr: - """Returns the average current limit, as an expression, derived from the inductor and switch (instantaneous) - current limits.""" - iout_limit_inductor = inductor_ilim - (inductor_iripple.upper() / 2) - iout_limit_sw = (sw_ilim.upper() > 0).then_else( - sw_ilim - (inductor_iripple.upper() / 2), Range.all()) - return iout_limit_inductor.intersect(iout_limit_sw).intersect(Range.from_lower(0)) - - def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequency: RangeLike, - output_current: RangeLike, sw_current_limits: RangeLike, *, - input_voltage_ripple: FloatLike, - output_voltage_ripple: FloatLike, - efficiency: RangeLike = (0.9, 1.0), # from TI reference - dutycycle_limit: RangeLike = (0.1, 0.9), - ripple_ratio: RangeLike = Range.all()): - super().__init__() - - self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # no modeling, input cap only - self.pwr_out = self.Port(VoltageSource.empty()) # models max output avg. current - # technically VoltageSink is the wrong model, but this is used to pass the current draw to the chip - # (and its input pin) without need the top-level to explicitly pass a parameter to the chip - self.switch = self.Port(VoltageSink.empty()) # models input / inductor avg. current draw - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input_voltage = self.ArgParameter(input_voltage) - self.output_voltage = self.ArgParameter(output_voltage) - self.frequency = self.ArgParameter(frequency) - self.output_current = self.ArgParameter(output_current) - self.sw_current_limits = self.ArgParameter(sw_current_limits) - - self.efficiency = self.ArgParameter(efficiency) - self.input_voltage_ripple = self.ArgParameter(input_voltage_ripple) - self.output_voltage_ripple = self.ArgParameter(output_voltage_ripple) - self.dutycycle_limit = self.ArgParameter(dutycycle_limit) - self.ripple_ratio = self.ArgParameter(ripple_ratio) # only used to force a ripple ratio at the actual currents - - self.generator_param(self.input_voltage, self.output_voltage, self.frequency, self.output_current, - self.sw_current_limits, self.input_voltage_ripple, self.output_voltage_ripple, - self.efficiency, self.dutycycle_limit, self.ripple_ratio) - - self.actual_dutycycle = self.Parameter(RangeExpr()) - self.actual_inductor_current_ripple = self.Parameter(RangeExpr()) - self.actual_inductor_current_peak = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "duty cycle: ", DescriptionString.FormatUnits(self.actual_dutycycle, ""), - " of limits: ", DescriptionString.FormatUnits(self.dutycycle_limit, ""), "\n", - "output current avg: ", DescriptionString.FormatUnits(self.output_current, "A"), - ", ripple: ", DescriptionString.FormatUnits(self.actual_inductor_current_ripple, "A") - ) - - @override - def generate(self) -> None: - super().generate() - values = self._calculate_parameters(self.get(self.input_voltage), self.get(self.output_voltage), - self.get(self.frequency), self.get(self.output_current), - self.get(self.sw_current_limits), self.get(self.ripple_ratio), - self.get(self.input_voltage_ripple), self.get(self.output_voltage_ripple), - efficiency=self.get(self.efficiency), - dutycycle_limit=self.get(self.dutycycle_limit)) - self.assign(self.actual_dutycycle, values.dutycycle) - self.require(values.dutycycle == values.effective_dutycycle, "dutycycle outside limit") - - self.inductor = self.Block(Inductor( - inductance=values.inductance * Henry, - current=values.inductor_avg_current, # min-bound only, the real filter happens in the filter_fn - frequency=self.frequency, - experimental_filter_fn=ExperimentalUserFnPartsTable.serialize_fn( - self._buck_inductor_filter, values.inductor_avg_current.upper, values.ripple_scale, values.min_ripple) - )) - self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance) - self.assign(self.actual_inductor_current_peak, - values.inductor_avg_current + self.actual_inductor_current_ripple / 2) - - self.connect(self.switch, self.inductor.a.adapt_to(VoltageSink( - current_draw=self.output_current * values.effective_dutycycle - ))) - self.connect(self.pwr_out, self.inductor.b.adapt_to(VoltageSource( - voltage_out=self.output_voltage, - current_limits=self._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits, - self.actual_inductor_current_ripple) * self.efficiency - ))) - - self.in_cap = self.Block(DecouplingCapacitor( - capacitance=values.input_capacitance * Farad, - exact_capacitance=True - )).connected(self.gnd, self.pwr_in) - self.out_cap = self.Block(DecouplingCapacitor( - capacitance=(Range.exact(float('inf')) * Farad).hull( - (values.output_capacitance_scale * self.actual_inductor_current_ripple.upper().max(values.min_ripple))), - exact_capacitance=True - )).connected(self.gnd, self.pwr_out) + + @staticmethod + def _d_inverse_d(d_range: Range) -> Range: + """Some power calculations require the maximum of D*(1-D), which has a maximum at D=0.5""" + # can't use range ops since they will double-count the tolerance of D, so calculate endpoints separately + range_endpoints = [d_range.lower * (1 - d_range.lower), d_range.upper * (1 - d_range.upper)] + raw_range = Range(min(range_endpoints), max(range_endpoints)) + if 0.5 in d_range: # the function has a maximum at 0.5 + return raw_range.hull(Range.exact(0.5 * (1 - 0.5))) + else: + return raw_range + + @staticmethod + def _ripple_current_from_sw_current(sw_current: float, ripple_ratio: Range) -> Range: + """Calculates the ripple current from a total switch current and ripple ratio.""" + return Range( # separate range parts to avoid double-counting tolerances + sw_current / (1 + ripple_ratio.lower) * ripple_ratio.lower, + sw_current / (1 + ripple_ratio.upper) * ripple_ratio.upper, + ) + + class Values(NamedTuple): + dutycycle: Range + inductance: Range + input_capacitance: Range + output_capacitance: Range + + inductor_avg_current: Range + ripple_scale: float # divide this by inductance to get the inductor ripple current + min_ripple: float # fallback minimum ripple current for component sizing for light-load, may be 0 + output_capacitance_scale: float # multiply inductor ripple by this to get required output capacitance + + inductor_peak_currents: Range # based on the worst case input spec, for unit testing + effective_dutycycle: Range # duty cycle adjusted for tracking behavior + + @classmethod + def _calculate_parameters( + cls, + input_voltage: Range, + output_voltage: Range, + frequency: Range, + output_current: Range, + sw_current_limits: Range, + ripple_ratio: Range, + input_voltage_ripple: float, + output_voltage_ripple: float, + efficiency: Range = Range(0.9, 1.0), + dutycycle_limit: Range = Range(0.1, 0.9), + limit_ripple_ratio: Range = Range(0.1, 0.5), + ) -> "BuckConverterPowerPath.Values": + """Calculates parameters for the buck converter power path. + + This uses the continuous conduction mode (CCM) equations to calculate component sizes. + DCM is not explicitly calculated since it requires additional parameters like minimum on-time. + The limit_ripple_ratio provides some broadly sane values for light-load / DCM operation. + This also ignores higher-order component behavior like capacitor ESR. + + The ripple_ratio is optional and may be set to Range.all(), allowing the inductor selector + to optimize the inductor by trading off inductance and max current. + + Values for component selections are bounded by: + - the ripple_ratio at output_current (if ripple_ratio < inf), and + - the limit_ripple_ratio at sw_current_limits (if sw_current_limits is not zero), as a fallback + for light-load conditions (where otherwise current goes to zero and inductance goes to the moon) + """ + dutycycle = output_voltage / input_voltage / efficiency + effective_dutycycle = dutycycle.bound_to(dutycycle_limit) # account for tracking behavior + + # calculate minimum inductance based on worst case values (operating range corners producing maximum inductance) + # worst-case input/output voltages and frequency is used to avoid double-counting tolerances as ranges + # note, for buck converter, L = (Vin - Vout) * D / (f * Iripple) = Vout (Vin - Vout) / (Iripple * f * Vin) + # this is at a maximum at Vin,max, and on that curve with a critical point at Vout = Vin,max / 2 + # note, the same formula calculates ripple-from-inductance and inductance-from-ripple + inductance_scale_candidates = [ + output_voltage.lower * (input_voltage.upper - output_voltage.lower) / input_voltage.upper, + output_voltage.upper * (input_voltage.upper - output_voltage.upper) / input_voltage.upper, + ] + if input_voltage.upper / 2 in output_voltage: + inductance_scale_candidates.append( + input_voltage.upper / 2 * (input_voltage.upper - input_voltage.upper / 2) / input_voltage.upper + ) + inductance_scale = max(inductance_scale_candidates) / frequency.lower + + inductance = Range.all() + min_ripple = 0.0 + if sw_current_limits.upper > 0: # fallback for light-load + ripple_current = cls._ripple_current_from_sw_current(sw_current_limits.upper, limit_ripple_ratio) + inductance = inductance.intersect(inductance_scale / ripple_current) + min_ripple = ripple_current.lower + if ripple_ratio.upper < float("inf"): + assert ripple_ratio.lower > 0, f"invalid non-inf ripple ratio {ripple_ratio}" + + inductance = inductance.intersect(inductance_scale / (output_current.upper * ripple_ratio)) + assert inductance.upper < float("inf"), "neither ripple_ratio nor fallback sw_current_limits given" + + input_capacitance = Range.from_lower( + output_current.upper + * cls._d_inverse_d(effective_dutycycle).upper + / (frequency.lower * input_voltage_ripple) + ) + output_capacitance_scale = 1 / (8 * frequency.lower * output_voltage_ripple) + + # these are static worst-case estimates for the range of specified ripple currents + # mainly used for unit testing + inductor_current_ripple = output_current * ripple_ratio.intersect(limit_ripple_ratio) + inductor_peak_currents = Range( + max(0, output_current.lower - inductor_current_ripple.upper / 2), + max(output_current.upper + inductor_current_ripple.upper / 2, inductor_current_ripple.upper), + ) + output_capacitance = Range.from_lower(output_capacitance_scale * inductor_current_ripple.upper) + + return cls.Values( + dutycycle=dutycycle, + inductance=inductance, + input_capacitance=input_capacitance, + output_capacitance=output_capacitance, + inductor_avg_current=output_current / efficiency, + ripple_scale=inductance_scale, + min_ripple=min_ripple, + output_capacitance_scale=output_capacitance_scale, + inductor_peak_currents=inductor_peak_currents, + effective_dutycycle=effective_dutycycle, + ) + + @staticmethod + @ExperimentalUserFnPartsTable.user_fn([float, float, float]) + def _buck_inductor_filter( + max_avg_current: float, ripple_scale: float, min_ripple: float + ) -> Callable[[PartsTableRow], bool]: + """Applies further filtering to inductors using the trade-off between inductance and peak-peak current. + max_avg_current is the maximum average current (not accounting for ripple) seen by the inductor + ripple_scale is the scaling factor from 1/L to ripple + This structure also works for boost converters, which would have its ripple_scale calculated differently.""" + + def filter_fn(row: PartsTableRow) -> bool: + ripple_current = max(ripple_scale / row[TableInductor.INDUCTANCE].lower, min_ripple) + max_current_pp = max_avg_current + ripple_current / 2 + return max_current_pp in row[TableInductor.CURRENT_RATING] + + return filter_fn + + @staticmethod + def _ilim_expr(inductor_ilim: RangeExpr, sw_ilim: RangeExpr, inductor_iripple: RangeExpr) -> RangeExpr: + """Returns the average current limit, as an expression, derived from the inductor and switch (instantaneous) + current limits.""" + iout_limit_inductor = inductor_ilim - (inductor_iripple.upper() / 2) + iout_limit_sw = (sw_ilim.upper() > 0).then_else(sw_ilim - (inductor_iripple.upper() / 2), Range.all()) + return iout_limit_inductor.intersect(iout_limit_sw).intersect(Range.from_lower(0)) + + def __init__( + self, + input_voltage: RangeLike, + output_voltage: RangeLike, + frequency: RangeLike, + output_current: RangeLike, + sw_current_limits: RangeLike, + *, + input_voltage_ripple: FloatLike, + output_voltage_ripple: FloatLike, + efficiency: RangeLike = (0.9, 1.0), # from TI reference + dutycycle_limit: RangeLike = (0.1, 0.9), + ripple_ratio: RangeLike = Range.all(), + ): + super().__init__() + + self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # no modeling, input cap only + self.pwr_out = self.Port(VoltageSource.empty()) # models max output avg. current + # technically VoltageSink is the wrong model, but this is used to pass the current draw to the chip + # (and its input pin) without need the top-level to explicitly pass a parameter to the chip + self.switch = self.Port(VoltageSink.empty()) # models input / inductor avg. current draw + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input_voltage = self.ArgParameter(input_voltage) + self.output_voltage = self.ArgParameter(output_voltage) + self.frequency = self.ArgParameter(frequency) + self.output_current = self.ArgParameter(output_current) + self.sw_current_limits = self.ArgParameter(sw_current_limits) + + self.efficiency = self.ArgParameter(efficiency) + self.input_voltage_ripple = self.ArgParameter(input_voltage_ripple) + self.output_voltage_ripple = self.ArgParameter(output_voltage_ripple) + self.dutycycle_limit = self.ArgParameter(dutycycle_limit) + self.ripple_ratio = self.ArgParameter(ripple_ratio) # only used to force a ripple ratio at the actual currents + + self.generator_param( + self.input_voltage, + self.output_voltage, + self.frequency, + self.output_current, + self.sw_current_limits, + self.input_voltage_ripple, + self.output_voltage_ripple, + self.efficiency, + self.dutycycle_limit, + self.ripple_ratio, + ) + + self.actual_dutycycle = self.Parameter(RangeExpr()) + self.actual_inductor_current_ripple = self.Parameter(RangeExpr()) + self.actual_inductor_current_peak = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "duty cycle: ", + DescriptionString.FormatUnits(self.actual_dutycycle, ""), + " of limits: ", + DescriptionString.FormatUnits(self.dutycycle_limit, ""), + "\n", + "output current avg: ", + DescriptionString.FormatUnits(self.output_current, "A"), + ", ripple: ", + DescriptionString.FormatUnits(self.actual_inductor_current_ripple, "A"), + ) + + @override + def generate(self) -> None: + super().generate() + values = self._calculate_parameters( + self.get(self.input_voltage), + self.get(self.output_voltage), + self.get(self.frequency), + self.get(self.output_current), + self.get(self.sw_current_limits), + self.get(self.ripple_ratio), + self.get(self.input_voltage_ripple), + self.get(self.output_voltage_ripple), + efficiency=self.get(self.efficiency), + dutycycle_limit=self.get(self.dutycycle_limit), + ) + self.assign(self.actual_dutycycle, values.dutycycle) + self.require(values.dutycycle == values.effective_dutycycle, "dutycycle outside limit") + + self.inductor = self.Block( + Inductor( + inductance=values.inductance * Henry, + current=values.inductor_avg_current, # min-bound only, the real filter happens in the filter_fn + frequency=self.frequency, + experimental_filter_fn=ExperimentalUserFnPartsTable.serialize_fn( + self._buck_inductor_filter, + values.inductor_avg_current.upper, + values.ripple_scale, + values.min_ripple, + ), + ) + ) + self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance) + self.assign( + self.actual_inductor_current_peak, values.inductor_avg_current + self.actual_inductor_current_ripple / 2 + ) + + self.connect( + self.switch, + self.inductor.a.adapt_to(VoltageSink(current_draw=self.output_current * values.effective_dutycycle)), + ) + self.connect( + self.pwr_out, + self.inductor.b.adapt_to( + VoltageSource( + voltage_out=self.output_voltage, + current_limits=self._ilim_expr( + self.inductor.actual_current_rating, self.sw_current_limits, self.actual_inductor_current_ripple + ) + * self.efficiency, + ) + ), + ) + + self.in_cap = self.Block( + DecouplingCapacitor(capacitance=values.input_capacitance * Farad, exact_capacitance=True) + ).connected(self.gnd, self.pwr_in) + self.out_cap = self.Block( + DecouplingCapacitor( + capacitance=(Range.exact(float("inf")) * Farad).hull( + ( + values.output_capacitance_scale + * self.actual_inductor_current_ripple.upper().max(values.min_ripple) + ) + ), + exact_capacitance=True, + ) + ).connected(self.gnd, self.pwr_out) @abstract_block_default(lambda: IdealBoostConverter) class BoostConverter(SwitchingVoltageRegulator): - """Step-up switching converter""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.require(self.pwr_out.voltage_out.lower() >= self.pwr_in.voltage_limits.lower()) + """Step-up switching converter""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.require(self.pwr_out.voltage_out.lower() >= self.pwr_in.voltage_limits.lower()) @abstract_block_default(lambda: IdealBoostConverter) class DiscreteBoostConverter(BoostConverter): - """Category for discrete boost converter subcircuits (as opposed to integrated components)""" + """Category for discrete boost converter subcircuits (as opposed to integrated components)""" class IdealBoostConverter(Resettable, DiscreteBoostConverter, IdealModel): - """Ideal boost converter producing the spec output voltage (buck-boost) limited by input voltage - and drawing input current from conversation of power""" - @override - def contents(self) -> None: - super().contents() - effective_output_voltage = self.output_voltage.intersect((self.pwr_in.link().voltage.lower(), float('inf'))) - self.gnd.init_from(Ground()) - self.pwr_in.init_from(VoltageSink( - current_draw=effective_output_voltage / self.pwr_in.link().voltage * self.pwr_out.link().current_drawn)) - self.pwr_out.init_from(VoltageSource( - voltage_out=effective_output_voltage)) - self.reset.init_from(DigitalSink()) + """Ideal boost converter producing the spec output voltage (buck-boost) limited by input voltage + and drawing input current from conversation of power""" + + @override + def contents(self) -> None: + super().contents() + effective_output_voltage = self.output_voltage.intersect((self.pwr_in.link().voltage.lower(), float("inf"))) + self.gnd.init_from(Ground()) + self.pwr_in.init_from( + VoltageSink( + current_draw=effective_output_voltage / self.pwr_in.link().voltage * self.pwr_out.link().current_drawn + ) + ) + self.pwr_out.init_from(VoltageSource(voltage_out=effective_output_voltage)) + self.reset.init_from(DigitalSink()) class BoostConverterPowerPath(InternalSubcircuit, GeneratorBlock): - """A helper block to generate the power path (inductors, capacitors) for a synchronous boost converter. - - Useful resources: - https://www.ti.com/lit/an/slva372c/slva372c.pdf - Component sizing in continuous mode - http://www.simonbramble.co.uk/dc_dc_converter_design/boost_converter/boost_converter_design.htm - Detailed analysis of converter with discrete FET and diode - """ - - class Values(NamedTuple): - dutycycle: Range - inductance: Range - input_capacitance: Range - output_capacitance: Range - - inductor_avg_current: Range - ripple_scale: float # divide this by inductance to get the inductor ripple current - min_ripple: float # fallback minimum ripple current for component sizing for light-load, may be 0 - - inductor_peak_currents: Range # based on the worst case input spec, for unit testing - effective_dutycycle: Range - - @classmethod - def _calculate_parameters(cls, input_voltage: Range, output_voltage: Range, frequency: Range, output_current: Range, - sw_current_limits: Range, ripple_ratio: Range, - input_voltage_ripple: float, output_voltage_ripple: float, - efficiency: Range = Range(0.8, 1.0), dutycycle_limit: Range = Range(0.1, 0.9), - limit_ripple_ratio: Range = Range(0.1, 0.5)) -> 'BoostConverterPowerPath.Values': - """See BuckConverterPowerPath._calculate_parameters, this performs a similar function.""" - dutycycle = 1 - input_voltage / output_voltage * efficiency - effective_dutycycle = dutycycle.bound_to(dutycycle_limit) # account for tracking behavior - inductor_avg_current = output_current / (1 - effective_dutycycle) - - # calculate minimum inductance based on worst case values (operating range corners producing maximum inductance) - # worst-case input/output voltages and frequency is used to avoid double-counting tolerances as ranges - # note, for boost converter, L = Vin * D / (f * Iripple) = Vin (Vout - Vin) / (Iripple * f * Vout) - # this is at a maximum at Vout,max, and on that curve with a critical point at Vin = Vout,max / 2 - inductance_scale_candidates = [ - input_voltage.lower * (output_voltage.upper - input_voltage.lower) / output_voltage.upper, - input_voltage.upper * (output_voltage.upper - input_voltage.upper) / output_voltage.upper, - ] - if output_voltage.upper / 2 in input_voltage: - inductance_scale_candidates.append( - output_voltage.upper/2 * (output_voltage.upper - output_voltage.upper/2) / output_voltage.upper) - inductance_scale = max(inductance_scale_candidates) / frequency.lower - - inductance = Range.all() - min_ripple = 0.0 - if sw_current_limits.upper > 0: # fallback for light-load - ripple_current = BuckConverterPowerPath._ripple_current_from_sw_current(sw_current_limits.upper, limit_ripple_ratio) - inductance = inductance.intersect(inductance_scale / ripple_current) - min_ripple = ripple_current.lower - if ripple_ratio.upper < float('inf'): - assert ripple_ratio.lower > 0, f"invalid non-inf ripple ratio {ripple_ratio}" - inductance = inductance.intersect(inductance_scale / (inductor_avg_current.upper * ripple_ratio)) - assert inductance.upper < float('inf'), 'neither ripple_ratio nor fallback sw_current_limits given' - - inductor_current_ripple = inductor_avg_current * ripple_ratio.intersect(limit_ripple_ratio) - inductor_peak_currents = Range(max(0, inductor_current_ripple.lower - inductor_current_ripple.upper / 2), - max(inductor_avg_current.upper + inductor_current_ripple.upper / 2, - inductor_current_ripple.upper)) - - # Capacitor equation Q = CV => i = C dv/dt => for constant current, i * t = C dV => dV = i * t / C - # C = i * t / dV => C = i / (f * dV) - # Boost converter draws current from input throughout the entire cycle, and by conversation of power - # the average input current is Iin = Vout/Vin * Iout = 1/(1-D) * Iout - # Boost converter current should be much less spikey than buck converter current and probably - # less filtering than this is acceptable - input_capacitance = Range.from_lower((output_current.upper / (1 - effective_dutycycle.upper)) / - (frequency.lower * input_voltage_ripple)) - output_capacitance = Range.from_lower(output_current.upper * effective_dutycycle.upper / - (frequency.lower * output_voltage_ripple)) - - return cls.Values(dutycycle=dutycycle, inductance=inductance, - input_capacitance=input_capacitance, output_capacitance=output_capacitance, - inductor_avg_current=inductor_avg_current, ripple_scale=inductance_scale, min_ripple=min_ripple, - inductor_peak_currents=inductor_peak_currents, - effective_dutycycle=effective_dutycycle) - - def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequency: RangeLike, - output_current: RangeLike, sw_current_limits: RangeLike, *, - input_voltage_ripple: FloatLike, - output_voltage_ripple: FloatLike, - efficiency: RangeLike = (0.8, 1.0), # from TI reference - dutycycle_limit: RangeLike = (0.1, 0.9), # arbitrary - ripple_ratio: RangeLike = Range.all()): - super().__init__() - - self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # models input / inductor avg. current draw - self.pwr_out = self.Port(VoltageSink.empty()) # no modeling, output cap only - self.switch = self.Port(VoltageSource.empty()) # models maximum output avg. current - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input_voltage = self.ArgParameter(input_voltage) - self.output_voltage = self.ArgParameter(output_voltage) - self.frequency = self.ArgParameter(frequency) - self.output_current = self.ArgParameter(output_current) - self.sw_current_limits = self.ArgParameter(sw_current_limits) - - self.efficiency = self.ArgParameter(efficiency) - self.input_voltage_ripple = self.ArgParameter(input_voltage_ripple) - self.output_voltage_ripple = self.ArgParameter(output_voltage_ripple) - self.dutycycle_limit = self.ArgParameter(dutycycle_limit) - self.ripple_ratio = self.ArgParameter(ripple_ratio) # only used to force a ripple ratio at the actual currents - - self.generator_param(self.input_voltage, self.output_voltage, self.frequency, self.output_current, - self.sw_current_limits, self.input_voltage_ripple, self.output_voltage_ripple, - self.efficiency, self.dutycycle_limit, self.ripple_ratio) - - self.actual_dutycycle = self.Parameter(RangeExpr()) - self.actual_inductor_current_ripple = self.Parameter(RangeExpr()) - self.actual_inductor_current_peak = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "duty cycle: ", DescriptionString.FormatUnits(self.actual_dutycycle, ""), - " of limits: ", DescriptionString.FormatUnits(self.dutycycle_limit, ""), "\n", - "output current avg: ", DescriptionString.FormatUnits(self.output_current, "A"), - ", ripple: ", DescriptionString.FormatUnits(self.actual_inductor_current_ripple, "A") - ) - - @override - def generate(self) -> None: - super().generate() - values = self._calculate_parameters(self.get(self.input_voltage), self.get(self.output_voltage), - self.get(self.frequency), self.get(self.output_current), - self.get(self.sw_current_limits), self.get(self.ripple_ratio), - self.get(self.input_voltage_ripple), self.get(self.output_voltage_ripple), - efficiency=self.get(self.efficiency), - dutycycle_limit=self.get(self.dutycycle_limit)) - self.assign(self.actual_dutycycle, values.dutycycle) - self.require(values.dutycycle == values.effective_dutycycle, "dutycycle outside limit") - - self.inductor = self.Block(Inductor( - inductance=values.inductance * Henry, - current=values.inductor_avg_current, # min-bound only, the real filter happens in the filter_fn - frequency=self.frequency, - experimental_filter_fn=ExperimentalUserFnPartsTable.serialize_fn( - BuckConverterPowerPath._buck_inductor_filter, - values.inductor_avg_current.upper, values.ripple_scale, values.min_ripple) - )) - self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance) - self.assign(self.actual_inductor_current_peak, - values.inductor_avg_current + self.actual_inductor_current_ripple / 2) - - self.connect(self.pwr_in, self.inductor.a.adapt_to(VoltageSink( - current_draw=values.inductor_avg_current - ))) - self.connect(self.switch, self.inductor.b.adapt_to(VoltageSource( - voltage_out=self.output_voltage, - current_limits=BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits, - self.actual_inductor_current_ripple) - * (1 - values.effective_dutycycle.upper) - ))) - - self.in_cap = self.Block(DecouplingCapacitor( - capacitance=values.input_capacitance * Farad, - exact_capacitance=True - )).connected(self.gnd, self.pwr_in) - - self.out_cap = self.Block(DecouplingCapacitor( - capacitance=values.output_capacitance * Farad, - exact_capacitance=True - )).connected(self.gnd, self.pwr_out) + """A helper block to generate the power path (inductors, capacitors) for a synchronous boost converter. + + Useful resources: + https://www.ti.com/lit/an/slva372c/slva372c.pdf + Component sizing in continuous mode + http://www.simonbramble.co.uk/dc_dc_converter_design/boost_converter/boost_converter_design.htm + Detailed analysis of converter with discrete FET and diode + """ + + class Values(NamedTuple): + dutycycle: Range + inductance: Range + input_capacitance: Range + output_capacitance: Range + + inductor_avg_current: Range + ripple_scale: float # divide this by inductance to get the inductor ripple current + min_ripple: float # fallback minimum ripple current for component sizing for light-load, may be 0 + + inductor_peak_currents: Range # based on the worst case input spec, for unit testing + effective_dutycycle: Range + + @classmethod + def _calculate_parameters( + cls, + input_voltage: Range, + output_voltage: Range, + frequency: Range, + output_current: Range, + sw_current_limits: Range, + ripple_ratio: Range, + input_voltage_ripple: float, + output_voltage_ripple: float, + efficiency: Range = Range(0.8, 1.0), + dutycycle_limit: Range = Range(0.1, 0.9), + limit_ripple_ratio: Range = Range(0.1, 0.5), + ) -> "BoostConverterPowerPath.Values": + """See BuckConverterPowerPath._calculate_parameters, this performs a similar function.""" + dutycycle = 1 - input_voltage / output_voltage * efficiency + effective_dutycycle = dutycycle.bound_to(dutycycle_limit) # account for tracking behavior + inductor_avg_current = output_current / (1 - effective_dutycycle) + + # calculate minimum inductance based on worst case values (operating range corners producing maximum inductance) + # worst-case input/output voltages and frequency is used to avoid double-counting tolerances as ranges + # note, for boost converter, L = Vin * D / (f * Iripple) = Vin (Vout - Vin) / (Iripple * f * Vout) + # this is at a maximum at Vout,max, and on that curve with a critical point at Vin = Vout,max / 2 + inductance_scale_candidates = [ + input_voltage.lower * (output_voltage.upper - input_voltage.lower) / output_voltage.upper, + input_voltage.upper * (output_voltage.upper - input_voltage.upper) / output_voltage.upper, + ] + if output_voltage.upper / 2 in input_voltage: + inductance_scale_candidates.append( + output_voltage.upper / 2 * (output_voltage.upper - output_voltage.upper / 2) / output_voltage.upper + ) + inductance_scale = max(inductance_scale_candidates) / frequency.lower + + inductance = Range.all() + min_ripple = 0.0 + if sw_current_limits.upper > 0: # fallback for light-load + ripple_current = BuckConverterPowerPath._ripple_current_from_sw_current( + sw_current_limits.upper, limit_ripple_ratio + ) + inductance = inductance.intersect(inductance_scale / ripple_current) + min_ripple = ripple_current.lower + if ripple_ratio.upper < float("inf"): + assert ripple_ratio.lower > 0, f"invalid non-inf ripple ratio {ripple_ratio}" + inductance = inductance.intersect(inductance_scale / (inductor_avg_current.upper * ripple_ratio)) + assert inductance.upper < float("inf"), "neither ripple_ratio nor fallback sw_current_limits given" + + inductor_current_ripple = inductor_avg_current * ripple_ratio.intersect(limit_ripple_ratio) + inductor_peak_currents = Range( + max(0, inductor_current_ripple.lower - inductor_current_ripple.upper / 2), + max(inductor_avg_current.upper + inductor_current_ripple.upper / 2, inductor_current_ripple.upper), + ) + + # Capacitor equation Q = CV => i = C dv/dt => for constant current, i * t = C dV => dV = i * t / C + # C = i * t / dV => C = i / (f * dV) + # Boost converter draws current from input throughout the entire cycle, and by conversation of power + # the average input current is Iin = Vout/Vin * Iout = 1/(1-D) * Iout + # Boost converter current should be much less spikey than buck converter current and probably + # less filtering than this is acceptable + input_capacitance = Range.from_lower( + (output_current.upper / (1 - effective_dutycycle.upper)) / (frequency.lower * input_voltage_ripple) + ) + output_capacitance = Range.from_lower( + output_current.upper * effective_dutycycle.upper / (frequency.lower * output_voltage_ripple) + ) + + return cls.Values( + dutycycle=dutycycle, + inductance=inductance, + input_capacitance=input_capacitance, + output_capacitance=output_capacitance, + inductor_avg_current=inductor_avg_current, + ripple_scale=inductance_scale, + min_ripple=min_ripple, + inductor_peak_currents=inductor_peak_currents, + effective_dutycycle=effective_dutycycle, + ) + + def __init__( + self, + input_voltage: RangeLike, + output_voltage: RangeLike, + frequency: RangeLike, + output_current: RangeLike, + sw_current_limits: RangeLike, + *, + input_voltage_ripple: FloatLike, + output_voltage_ripple: FloatLike, + efficiency: RangeLike = (0.8, 1.0), # from TI reference + dutycycle_limit: RangeLike = (0.1, 0.9), # arbitrary + ripple_ratio: RangeLike = Range.all(), + ): + super().__init__() + + self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # models input / inductor avg. current draw + self.pwr_out = self.Port(VoltageSink.empty()) # no modeling, output cap only + self.switch = self.Port(VoltageSource.empty()) # models maximum output avg. current + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input_voltage = self.ArgParameter(input_voltage) + self.output_voltage = self.ArgParameter(output_voltage) + self.frequency = self.ArgParameter(frequency) + self.output_current = self.ArgParameter(output_current) + self.sw_current_limits = self.ArgParameter(sw_current_limits) + + self.efficiency = self.ArgParameter(efficiency) + self.input_voltage_ripple = self.ArgParameter(input_voltage_ripple) + self.output_voltage_ripple = self.ArgParameter(output_voltage_ripple) + self.dutycycle_limit = self.ArgParameter(dutycycle_limit) + self.ripple_ratio = self.ArgParameter(ripple_ratio) # only used to force a ripple ratio at the actual currents + + self.generator_param( + self.input_voltage, + self.output_voltage, + self.frequency, + self.output_current, + self.sw_current_limits, + self.input_voltage_ripple, + self.output_voltage_ripple, + self.efficiency, + self.dutycycle_limit, + self.ripple_ratio, + ) + + self.actual_dutycycle = self.Parameter(RangeExpr()) + self.actual_inductor_current_ripple = self.Parameter(RangeExpr()) + self.actual_inductor_current_peak = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "duty cycle: ", + DescriptionString.FormatUnits(self.actual_dutycycle, ""), + " of limits: ", + DescriptionString.FormatUnits(self.dutycycle_limit, ""), + "\n", + "output current avg: ", + DescriptionString.FormatUnits(self.output_current, "A"), + ", ripple: ", + DescriptionString.FormatUnits(self.actual_inductor_current_ripple, "A"), + ) + + @override + def generate(self) -> None: + super().generate() + values = self._calculate_parameters( + self.get(self.input_voltage), + self.get(self.output_voltage), + self.get(self.frequency), + self.get(self.output_current), + self.get(self.sw_current_limits), + self.get(self.ripple_ratio), + self.get(self.input_voltage_ripple), + self.get(self.output_voltage_ripple), + efficiency=self.get(self.efficiency), + dutycycle_limit=self.get(self.dutycycle_limit), + ) + self.assign(self.actual_dutycycle, values.dutycycle) + self.require(values.dutycycle == values.effective_dutycycle, "dutycycle outside limit") + + self.inductor = self.Block( + Inductor( + inductance=values.inductance * Henry, + current=values.inductor_avg_current, # min-bound only, the real filter happens in the filter_fn + frequency=self.frequency, + experimental_filter_fn=ExperimentalUserFnPartsTable.serialize_fn( + BuckConverterPowerPath._buck_inductor_filter, + values.inductor_avg_current.upper, + values.ripple_scale, + values.min_ripple, + ), + ) + ) + self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance) + self.assign( + self.actual_inductor_current_peak, values.inductor_avg_current + self.actual_inductor_current_ripple / 2 + ) + + self.connect(self.pwr_in, self.inductor.a.adapt_to(VoltageSink(current_draw=values.inductor_avg_current))) + self.connect( + self.switch, + self.inductor.b.adapt_to( + VoltageSource( + voltage_out=self.output_voltage, + current_limits=BuckConverterPowerPath._ilim_expr( + self.inductor.actual_current_rating, self.sw_current_limits, self.actual_inductor_current_ripple + ) + * (1 - values.effective_dutycycle.upper), + ) + ), + ) + + self.in_cap = self.Block( + DecouplingCapacitor(capacitance=values.input_capacitance * Farad, exact_capacitance=True) + ).connected(self.gnd, self.pwr_in) + + self.out_cap = self.Block( + DecouplingCapacitor(capacitance=values.output_capacitance * Farad, exact_capacitance=True) + ).connected(self.gnd, self.pwr_out) @abstract_block_default(lambda: IdealVoltageRegulator) class BuckBoostConverter(SwitchingVoltageRegulator): - """Step-up or switch-down switching converter""" + """Step-up or switch-down switching converter""" @abstract_block_default(lambda: IdealVoltageRegulator) class DiscreteBuckBoostConverter(BuckBoostConverter): - """Category for discrete buck-boost converter subcircuits (as opposed to integrated components)""" + """Category for discrete buck-boost converter subcircuits (as opposed to integrated components)""" class IdealVoltageRegulator(Resettable, DiscreteBuckBoostConverter, IdealModel): - """Ideal buck-boost / general DC-DC converter producing the spec output voltage - and drawing input current from conversation of power""" - @override - def contents(self) -> None: - super().contents() - self.gnd.init_from(Ground()) - self.pwr_in.init_from(VoltageSink( - current_draw=self.output_voltage / self.pwr_in.link().voltage * self.pwr_out.link().current_drawn)) - self.pwr_out.init_from(VoltageSource( - voltage_out=self.output_voltage)) - self.reset.init_from(DigitalSink()) + """Ideal buck-boost / general DC-DC converter producing the spec output voltage + and drawing input current from conversation of power""" + + @override + def contents(self) -> None: + super().contents() + self.gnd.init_from(Ground()) + self.pwr_in.init_from( + VoltageSink( + current_draw=self.output_voltage / self.pwr_in.link().voltage * self.pwr_out.link().current_drawn + ) + ) + self.pwr_out.init_from(VoltageSource(voltage_out=self.output_voltage)) + self.reset.init_from(DigitalSink()) class BuckBoostConverterPowerPath(InternalSubcircuit, GeneratorBlock): - """A helper block to generate the power path (inductors, capacitors) for a 4-switch buck-boost converter. - - Main assumptions in component sizing - - Operating only in continuous mode, TODO: also consider boundary and discontinuous mode - - TODO: account for capacitor ESR? - - Useful resources: - https://www.ti.com/lit/an/slva535b/slva535b.pdf - Largely based on this document, the tl;dr of which is combine the buck and boost equations - """ - def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequency: RangeLike, - output_current: RangeLike, sw_current_limits: RangeLike, *, - efficiency: RangeLike = (0.8, 1.0), # from TI reference - input_voltage_ripple: FloatLike = 75*mVolt, - output_voltage_ripple: FloatLike = 25*mVolt, # arbitrary - ripple_ratio: RangeLike = Range.all()): - super().__init__() - - self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # no modeling, input cap only - self.switch_in = self.Port(VoltageSink.empty()) # models input / inductor avg. current draw - self.switch_out = self.Port(VoltageSource.empty()) # models maximum output avg. current - self.pwr_out = self.Port(VoltageSink.empty()) # no modeling, output cap only - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input_voltage = self.ArgParameter(input_voltage) - self.output_voltage = self.ArgParameter(output_voltage) - self.frequency = self.ArgParameter(frequency) - self.output_current = self.ArgParameter(output_current) - self.sw_current_limits = self.ArgParameter(sw_current_limits) - self.efficiency = self.ArgParameter(efficiency) - self.input_voltage_ripple = self.ArgParameter(input_voltage_ripple) - self.output_voltage_ripple = self.ArgParameter(output_voltage_ripple) - self.ripple_ratio = self.ArgParameter(ripple_ratio) # only used to force a ripple ratio at the actual currents - - # duty cycle limits not supported, since the crossover point has a dutycycle of 0 (boost) and 1 (buck) - self.generator_param(self.input_voltage, self.output_voltage, self.frequency, self.output_current, - self.sw_current_limits, self.input_voltage_ripple, self.output_voltage_ripple, - self.efficiency, self.ripple_ratio) - - self.actual_buck_dutycycle = self.Parameter(RangeExpr()) # possible actual duty cycle in buck mode - self.actual_boost_dutycycle = self.Parameter(RangeExpr()) # possible actual duty cycle in boost mode - self.actual_inductor_current_ripple = self.Parameter(RangeExpr()) - self.actual_inductor_current_peak = self.Parameter(RangeExpr()) # inductor current accounting for ripple (upper is peak) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "duty cycle: ", DescriptionString.FormatUnits(self.actual_buck_dutycycle, ""), " (buck)", - ", ", DescriptionString.FormatUnits(self.actual_boost_dutycycle, ""), " (boost)\n", - "output current avg: ", DescriptionString.FormatUnits(self.output_current, "A"), - ", ripple: ", DescriptionString.FormatUnits(self.actual_inductor_current_ripple, "A") - ) - - @override - def generate(self) -> None: - super().generate() - buck_values = BuckConverterPowerPath._calculate_parameters( - self.get(self.input_voltage), self.get(self.output_voltage), - self.get(self.frequency), self.get(self.output_current), - self.get(self.sw_current_limits), self.get(self.ripple_ratio), - self.get(self.input_voltage_ripple), self.get(self.output_voltage_ripple), - efficiency=self.get(self.efficiency), dutycycle_limit=Range(0, 1)) - boost_values = BoostConverterPowerPath._calculate_parameters( - self.get(self.input_voltage), self.get(self.output_voltage), - self.get(self.frequency), self.get(self.output_current), - self.get(self.sw_current_limits), self.get(self.ripple_ratio), - self.get(self.input_voltage_ripple), self.get(self.output_voltage_ripple), - efficiency=self.get(self.efficiency), dutycycle_limit=Range(0, 1)) - self.assign(self.actual_buck_dutycycle, buck_values.effective_dutycycle) - self.assign(self.actual_boost_dutycycle, boost_values.effective_dutycycle) - - combined_ripple_scale = max(buck_values.ripple_scale, boost_values.ripple_scale) - combined_inductor_avg_current = boost_values.inductor_avg_current.hull(boost_values.inductor_avg_current) - combined_min_ripple = max(buck_values.min_ripple, boost_values.min_ripple) - - self.inductor = self.Block(Inductor( - inductance=buck_values.inductance.intersect(boost_values.inductance) * Henry, - current=buck_values.inductor_avg_current.hull(boost_values.inductor_avg_current), - frequency=self.frequency, - experimental_filter_fn=ExperimentalUserFnPartsTable.serialize_fn( - BuckConverterPowerPath._buck_inductor_filter, - combined_inductor_avg_current.upper, combined_ripple_scale, combined_min_ripple) - )) - self.connect(self.switch_in, self.inductor.a.adapt_to(VoltageSink( - current_draw=combined_inductor_avg_current - ))) - self.connect(self.switch_out, self.inductor.b.adapt_to(VoltageSource( - voltage_out=self.output_voltage, - current_limits=BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits, - self.actual_inductor_current_ripple) - * (1 - boost_values.effective_dutycycle.upper) - ))) - self.assign(self.actual_inductor_current_ripple, combined_ripple_scale / self.inductor.actual_inductance) - self.assign(self.actual_inductor_current_peak, - combined_inductor_avg_current + self.actual_inductor_current_ripple / 2) - - self.in_cap = self.Block(DecouplingCapacitor( - capacitance=buck_values.input_capacitance.intersect(boost_values.input_capacitance) * Farad, - exact_capacitance=True - )).connected(self.gnd, self.pwr_in) - self.out_cap = self.Block(DecouplingCapacitor( - capacitance=(Range.exact(float('inf')) * Farad).hull( - (buck_values.output_capacitance_scale * self.actual_inductor_current_ripple.upper()).max( - boost_values.output_capacitance.lower) - ), - exact_capacitance=True - )).connected(self.gnd, self.pwr_out) + """A helper block to generate the power path (inductors, capacitors) for a 4-switch buck-boost converter. + + Main assumptions in component sizing + - Operating only in continuous mode, TODO: also consider boundary and discontinuous mode + - TODO: account for capacitor ESR? + + Useful resources: + https://www.ti.com/lit/an/slva535b/slva535b.pdf + Largely based on this document, the tl;dr of which is combine the buck and boost equations + """ + + def __init__( + self, + input_voltage: RangeLike, + output_voltage: RangeLike, + frequency: RangeLike, + output_current: RangeLike, + sw_current_limits: RangeLike, + *, + efficiency: RangeLike = (0.8, 1.0), # from TI reference + input_voltage_ripple: FloatLike = 75 * mVolt, + output_voltage_ripple: FloatLike = 25 * mVolt, # arbitrary + ripple_ratio: RangeLike = Range.all(), + ): + super().__init__() + + self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # no modeling, input cap only + self.switch_in = self.Port(VoltageSink.empty()) # models input / inductor avg. current draw + self.switch_out = self.Port(VoltageSource.empty()) # models maximum output avg. current + self.pwr_out = self.Port(VoltageSink.empty()) # no modeling, output cap only + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input_voltage = self.ArgParameter(input_voltage) + self.output_voltage = self.ArgParameter(output_voltage) + self.frequency = self.ArgParameter(frequency) + self.output_current = self.ArgParameter(output_current) + self.sw_current_limits = self.ArgParameter(sw_current_limits) + self.efficiency = self.ArgParameter(efficiency) + self.input_voltage_ripple = self.ArgParameter(input_voltage_ripple) + self.output_voltage_ripple = self.ArgParameter(output_voltage_ripple) + self.ripple_ratio = self.ArgParameter(ripple_ratio) # only used to force a ripple ratio at the actual currents + + # duty cycle limits not supported, since the crossover point has a dutycycle of 0 (boost) and 1 (buck) + self.generator_param( + self.input_voltage, + self.output_voltage, + self.frequency, + self.output_current, + self.sw_current_limits, + self.input_voltage_ripple, + self.output_voltage_ripple, + self.efficiency, + self.ripple_ratio, + ) + + self.actual_buck_dutycycle = self.Parameter(RangeExpr()) # possible actual duty cycle in buck mode + self.actual_boost_dutycycle = self.Parameter(RangeExpr()) # possible actual duty cycle in boost mode + self.actual_inductor_current_ripple = self.Parameter(RangeExpr()) + self.actual_inductor_current_peak = self.Parameter( + RangeExpr() + ) # inductor current accounting for ripple (upper is peak) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "duty cycle: ", + DescriptionString.FormatUnits(self.actual_buck_dutycycle, ""), + " (buck)", + ", ", + DescriptionString.FormatUnits(self.actual_boost_dutycycle, ""), + " (boost)\n", + "output current avg: ", + DescriptionString.FormatUnits(self.output_current, "A"), + ", ripple: ", + DescriptionString.FormatUnits(self.actual_inductor_current_ripple, "A"), + ) + + @override + def generate(self) -> None: + super().generate() + buck_values = BuckConverterPowerPath._calculate_parameters( + self.get(self.input_voltage), + self.get(self.output_voltage), + self.get(self.frequency), + self.get(self.output_current), + self.get(self.sw_current_limits), + self.get(self.ripple_ratio), + self.get(self.input_voltage_ripple), + self.get(self.output_voltage_ripple), + efficiency=self.get(self.efficiency), + dutycycle_limit=Range(0, 1), + ) + boost_values = BoostConverterPowerPath._calculate_parameters( + self.get(self.input_voltage), + self.get(self.output_voltage), + self.get(self.frequency), + self.get(self.output_current), + self.get(self.sw_current_limits), + self.get(self.ripple_ratio), + self.get(self.input_voltage_ripple), + self.get(self.output_voltage_ripple), + efficiency=self.get(self.efficiency), + dutycycle_limit=Range(0, 1), + ) + self.assign(self.actual_buck_dutycycle, buck_values.effective_dutycycle) + self.assign(self.actual_boost_dutycycle, boost_values.effective_dutycycle) + + combined_ripple_scale = max(buck_values.ripple_scale, boost_values.ripple_scale) + combined_inductor_avg_current = boost_values.inductor_avg_current.hull(boost_values.inductor_avg_current) + combined_min_ripple = max(buck_values.min_ripple, boost_values.min_ripple) + + self.inductor = self.Block( + Inductor( + inductance=buck_values.inductance.intersect(boost_values.inductance) * Henry, + current=buck_values.inductor_avg_current.hull(boost_values.inductor_avg_current), + frequency=self.frequency, + experimental_filter_fn=ExperimentalUserFnPartsTable.serialize_fn( + BuckConverterPowerPath._buck_inductor_filter, + combined_inductor_avg_current.upper, + combined_ripple_scale, + combined_min_ripple, + ), + ) + ) + self.connect(self.switch_in, self.inductor.a.adapt_to(VoltageSink(current_draw=combined_inductor_avg_current))) + self.connect( + self.switch_out, + self.inductor.b.adapt_to( + VoltageSource( + voltage_out=self.output_voltage, + current_limits=BuckConverterPowerPath._ilim_expr( + self.inductor.actual_current_rating, self.sw_current_limits, self.actual_inductor_current_ripple + ) + * (1 - boost_values.effective_dutycycle.upper), + ) + ), + ) + self.assign(self.actual_inductor_current_ripple, combined_ripple_scale / self.inductor.actual_inductance) + self.assign( + self.actual_inductor_current_peak, combined_inductor_avg_current + self.actual_inductor_current_ripple / 2 + ) + + self.in_cap = self.Block( + DecouplingCapacitor( + capacitance=buck_values.input_capacitance.intersect(boost_values.input_capacitance) * Farad, + exact_capacitance=True, + ) + ).connected(self.gnd, self.pwr_in) + self.out_cap = self.Block( + DecouplingCapacitor( + capacitance=(Range.exact(float("inf")) * Farad).hull( + (buck_values.output_capacitance_scale * self.actual_inductor_current_ripple.upper()).max( + boost_values.output_capacitance.lower + ) + ), + exact_capacitance=True, + ) + ).connected(self.gnd, self.pwr_out) diff --git a/edg/abstract_parts/AbstractResistor.py b/edg/abstract_parts/AbstractResistor.py index 693304773..264ab95a5 100644 --- a/edg/abstract_parts/AbstractResistor.py +++ b/edg/abstract_parts/AbstractResistor.py @@ -13,424 +13,483 @@ @abstract_block class Resistor(PassiveComponent, KiCadInstantiableBlock, HasStandardFootprint): - _STANDARD_FOOTPRINT = lambda: ResistorStandardFootprint - - RESISTOR_REGEX = re.compile("^" + f"([\d.{PartParserUtil.SI_PREFIXES}]+(?:\s*[{PartParserUtil.SI_PREFIXES}])?)\s*[RΩ]?" + - "\s*" + "((?:\+-|\+/-|±)?\s*[\d.]+\s*%?)?" + "$") - RESISTOR_DEFAULT_TOL = 0.05 # TODO this should be unified elsewhere - - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name in ('Device:R', 'Device:R_Small') - return {'1': self.a, '2': self.b} - - @classmethod - def parse_resistor(cls, value: str) -> Range: - match = cls.RESISTOR_REGEX.match(value) - assert match is not None, f"could not parse resistor from value '{value}'" - center = PartParserUtil.parse_value(match.group(1), '') - if match.group(2) is not None: - tol_str = match.group(2) - if not tol_str.startswith('±'): # format conversion to more strict parser - tol_str = '±' + tol_str - return PartParserUtil.parse_abs_tolerance(tol_str, center, 'Ω') - else: - return Range.from_tolerance(center, (-cls.RESISTOR_DEFAULT_TOL, cls.RESISTOR_DEFAULT_TOL)) - - @classmethod - @override - def block_from_symbol(cls, symbol_name: str, properties: Mapping[str, str]) -> 'Resistor': - return Resistor(resistance=cls.parse_resistor(properties['Value'])) - - def __init__(self, resistance: RangeLike, power: RangeLike = RangeExpr.ZERO, - voltage: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() - - self.a = self.Port(Passive.empty()) - self.b = self.Port(Passive.empty()) - - self.resistance = self.ArgParameter(resistance) - self.power = self.ArgParameter(power) # operating power range - self.voltage = self.ArgParameter(voltage) # operating voltage range - self.actual_resistance = self.Parameter(RangeExpr()) - self.actual_power_rating = self.Parameter(RangeExpr()) - self.actual_voltage_rating = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "resistance: ", DescriptionString.FormatUnits(self.actual_resistance, "Ω"), - " of spec ", DescriptionString.FormatUnits(self.resistance, "Ω"), "\n", - "power rating: ", DescriptionString.FormatUnits(self.actual_power_rating, "W"), - " of operating: ", DescriptionString.FormatUnits(self.power, "W"), "\n", - "voltage rating: ", DescriptionString.FormatUnits(self.actual_voltage_rating, "V"), - " of operating: ", DescriptionString.FormatUnits(self.voltage, "V") + _STANDARD_FOOTPRINT = lambda: ResistorStandardFootprint + + RESISTOR_REGEX = re.compile( + "^" + + f"([\d.{PartParserUtil.SI_PREFIXES}]+(?:\s*[{PartParserUtil.SI_PREFIXES}])?)\s*[RΩ]?" + + "\s*" + + "((?:\+-|\+/-|±)?\s*[\d.]+\s*%?)?" + + "$" ) + RESISTOR_DEFAULT_TOL = 0.05 # TODO this should be unified elsewhere + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name in ("Device:R", "Device:R_Small") + return {"1": self.a, "2": self.b} + + @classmethod + def parse_resistor(cls, value: str) -> Range: + match = cls.RESISTOR_REGEX.match(value) + assert match is not None, f"could not parse resistor from value '{value}'" + center = PartParserUtil.parse_value(match.group(1), "") + if match.group(2) is not None: + tol_str = match.group(2) + if not tol_str.startswith("±"): # format conversion to more strict parser + tol_str = "±" + tol_str + return PartParserUtil.parse_abs_tolerance(tol_str, center, "Ω") + else: + return Range.from_tolerance(center, (-cls.RESISTOR_DEFAULT_TOL, cls.RESISTOR_DEFAULT_TOL)) + + @classmethod + @override + def block_from_symbol(cls, symbol_name: str, properties: Mapping[str, str]) -> "Resistor": + return Resistor(resistance=cls.parse_resistor(properties["Value"])) + + def __init__( + self, resistance: RangeLike, power: RangeLike = RangeExpr.ZERO, voltage: RangeLike = RangeExpr.ZERO + ) -> None: + super().__init__() + + self.a = self.Port(Passive.empty()) + self.b = self.Port(Passive.empty()) + + self.resistance = self.ArgParameter(resistance) + self.power = self.ArgParameter(power) # operating power range + self.voltage = self.ArgParameter(voltage) # operating voltage range + self.actual_resistance = self.Parameter(RangeExpr()) + self.actual_power_rating = self.Parameter(RangeExpr()) + self.actual_voltage_rating = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "resistance: ", + DescriptionString.FormatUnits(self.actual_resistance, "Ω"), + " of spec ", + DescriptionString.FormatUnits(self.resistance, "Ω"), + "\n", + "power rating: ", + DescriptionString.FormatUnits(self.actual_power_rating, "W"), + " of operating: ", + DescriptionString.FormatUnits(self.power, "W"), + "\n", + "voltage rating: ", + DescriptionString.FormatUnits(self.actual_voltage_rating, "V"), + " of operating: ", + DescriptionString.FormatUnits(self.voltage, "V"), + ) class ResistorStandardFootprint(StandardFootprint[Resistor]): - REFDES_PREFIX = 'R' - - FOOTPRINT_PINNING_MAP = { - ( - 'Resistor_SMD:R_0201_0603Metric', - 'Resistor_SMD:R_0402_1005Metric', - 'Resistor_SMD:R_0603_1608Metric', - 'Resistor_SMD:R_0805_2012Metric', - 'Resistor_SMD:R_1206_3216Metric', - 'Resistor_SMD:R_1210_3225Metric', - 'Resistor_SMD:R_2010_5025Metric', - 'Resistor_SMD:R_2512_6332Metric', - - 'Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P5.08mm_Horizontal', - 'Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal', - 'Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P12.70mm_Horizontal', - 'Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P12.70mm_Horizontal', - 'Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P15.24mm_Horizontal', - - 'Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P1.90mm_Vertical', - 'Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical', - 'Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P2.54mm_Vertical', - 'Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P5.08mm_Vertical', - 'Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P5.08mm_Vertical', - ): lambda block: { - '1': block.a, - '2': block.b, - }, - } + REFDES_PREFIX = "R" + + FOOTPRINT_PINNING_MAP = { + ( + "Resistor_SMD:R_0201_0603Metric", + "Resistor_SMD:R_0402_1005Metric", + "Resistor_SMD:R_0603_1608Metric", + "Resistor_SMD:R_0805_2012Metric", + "Resistor_SMD:R_1206_3216Metric", + "Resistor_SMD:R_1210_3225Metric", + "Resistor_SMD:R_2010_5025Metric", + "Resistor_SMD:R_2512_6332Metric", + "Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P5.08mm_Horizontal", + "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", + "Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P12.70mm_Horizontal", + "Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P12.70mm_Horizontal", + "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P15.24mm_Horizontal", + "Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P1.90mm_Vertical", + "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical", + "Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P2.54mm_Vertical", + "Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P5.08mm_Vertical", + "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P5.08mm_Vertical", + ): lambda block: { + "1": block.a, + "2": block.b, + }, + } @non_library class TableResistor(PartsTableSelector, Resistor): - RESISTANCE = PartsTableColumn(Range) - POWER_RATING = PartsTableColumn(Range) - VOLTAGE_RATING = PartsTableColumn(Range) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.resistance, self.power, self.voltage) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - row[self.RESISTANCE].fuzzy_in(self.get(self.resistance)) and \ - self.get(self.power).fuzzy_in(row[self.POWER_RATING]) and \ - self.get(self.voltage).fuzzy_in(row[self.VOLTAGE_RATING]) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.actual_resistance, row[self.RESISTANCE]) - self.assign(self.actual_power_rating, row[self.POWER_RATING]) - self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) - - @classmethod - @override - def _row_sort_by(cls, row: PartsTableRow) -> Any: - return (ESeriesUtil.series_of(row[cls.RESISTANCE].center(), default=ESeriesUtil.SERIES_MAX + 1), - super()._row_sort_by(row)) + RESISTANCE = PartsTableColumn(Range) + POWER_RATING = PartsTableColumn(Range) + VOLTAGE_RATING = PartsTableColumn(Range) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.resistance, self.power, self.voltage) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return ( + super()._row_filter(row) + and row[self.RESISTANCE].fuzzy_in(self.get(self.resistance)) + and self.get(self.power).fuzzy_in(row[self.POWER_RATING]) + and self.get(self.voltage).fuzzy_in(row[self.VOLTAGE_RATING]) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.actual_resistance, row[self.RESISTANCE]) + self.assign(self.actual_power_rating, row[self.POWER_RATING]) + self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING]) + + @classmethod + @override + def _row_sort_by(cls, row: PartsTableRow) -> Any: + return ( + ESeriesUtil.series_of(row[cls.RESISTANCE].center(), default=ESeriesUtil.SERIES_MAX + 1), + super()._row_sort_by(row), + ) class SeriesResistor(Resistor, GeneratorBlock): - """Splits a resistor into equal resistors in series. Improves power and voltage ratings - by distributing the load across multiple devices. - - Generally used as a refinement to break up a single (logical) resistor that is dissipating too much power - or has an excessive voltage across it. Accounts for tolerance stackup for power and voltage distribution - using specified (not actual) resistor tolerance - is a pessimistic calculation.""" - def __init__(self, *args: Any, count: IntLike = 2, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.count = self.ArgParameter(count) - self.generator_param(self.count, self.resistance) - - @override - def generate(self) -> None: - super().generate() - count = self.get(self.count) - last_port = self.a - cumu_resistance: RangeLike = Range.exact(0) - cumu_power_rating: RangeLike = Range.exact(0) - cumu_voltage_rating: RangeLike = Range.exact(0) - self.res = ElementDict[Resistor]() - - # calculate tolerance stackup effects on R for worst-case power and voltage - resistance_range = self.get(self.resistance) - resistance_tol = (resistance_range.upper - resistance_range.lower) / 2 / resistance_range.center() - resistance_tol = min(0.05, resistance_tol) # in practice there should be no >5% resistors - resistance_ratio_range = Range((1 - resistance_tol) / (count + resistance_tol * (count - 2)), - (1 + resistance_tol) / (count - resistance_tol * (count - 2))) - - elt_resistance = resistance_range / count - elt_power = self.power * resistance_ratio_range - elt_voltage = self.voltage * resistance_ratio_range - - for i in range(count): - self.res[i] = res = self.Block(Resistor(resistance=elt_resistance, - power=elt_power, - voltage=elt_voltage)) - self.connect(last_port, res.a) - cumu_resistance = cumu_resistance + res.actual_resistance - cumu_power_rating = cumu_power_rating + res.actual_power_rating - cumu_voltage_rating = cumu_voltage_rating + res.actual_voltage_rating - last_port = res.b - self.connect(last_port, self.b) - self.assign(self.actual_resistance, cumu_resistance) - self.assign(self.actual_power_rating, cumu_power_rating) - self.assign(self.actual_voltage_rating, cumu_voltage_rating) + """Splits a resistor into equal resistors in series. Improves power and voltage ratings + by distributing the load across multiple devices. + + Generally used as a refinement to break up a single (logical) resistor that is dissipating too much power + or has an excessive voltage across it. Accounts for tolerance stackup for power and voltage distribution + using specified (not actual) resistor tolerance - is a pessimistic calculation.""" + + def __init__(self, *args: Any, count: IntLike = 2, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.count = self.ArgParameter(count) + self.generator_param(self.count, self.resistance) + + @override + def generate(self) -> None: + super().generate() + count = self.get(self.count) + last_port = self.a + cumu_resistance: RangeLike = Range.exact(0) + cumu_power_rating: RangeLike = Range.exact(0) + cumu_voltage_rating: RangeLike = Range.exact(0) + self.res = ElementDict[Resistor]() + + # calculate tolerance stackup effects on R for worst-case power and voltage + resistance_range = self.get(self.resistance) + resistance_tol = (resistance_range.upper - resistance_range.lower) / 2 / resistance_range.center() + resistance_tol = min(0.05, resistance_tol) # in practice there should be no >5% resistors + resistance_ratio_range = Range( + (1 - resistance_tol) / (count + resistance_tol * (count - 2)), + (1 + resistance_tol) / (count - resistance_tol * (count - 2)), + ) + + elt_resistance = resistance_range / count + elt_power = self.power * resistance_ratio_range + elt_voltage = self.voltage * resistance_ratio_range + + for i in range(count): + self.res[i] = res = self.Block(Resistor(resistance=elt_resistance, power=elt_power, voltage=elt_voltage)) + self.connect(last_port, res.a) + cumu_resistance = cumu_resistance + res.actual_resistance + cumu_power_rating = cumu_power_rating + res.actual_power_rating + cumu_voltage_rating = cumu_voltage_rating + res.actual_voltage_rating + last_port = res.b + self.connect(last_port, self.b) + self.assign(self.actual_resistance, cumu_resistance) + self.assign(self.actual_power_rating, cumu_power_rating) + self.assign(self.actual_voltage_rating, cumu_voltage_rating) class PullupResistor(DiscreteApplication): - """Pull-up resistor with an VoltageSink for automatic implicit connect to a Power line.""" - def __init__(self, resistance: RangeLike) -> None: - super().__init__() + """Pull-up resistor with an VoltageSink for automatic implicit connect to a Power line.""" - self.res = self.Block(Resistor(resistance, 0*Watt(tol=0))) # TODO automatically calculate power + def __init__(self, resistance: RangeLike) -> None: + super().__init__() - self.pwr = self.Export(self.res.a.adapt_to(VoltageSink()), [Power]) - self.io = self.Export(self.res.b.adapt_to( - DigitalSource.pullup_from_supply(self.pwr) - ), [InOut]) + self.res = self.Block(Resistor(resistance, 0 * Watt(tol=0))) # TODO automatically calculate power - def connected(self, pwr: Optional[Port[VoltageLink]] = None, io: Optional[Port[DigitalLink]] = None) -> \ - 'PullupResistor': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if pwr is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr, self.pwr) - if io is not None: - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + self.pwr = self.Export(self.res.a.adapt_to(VoltageSink()), [Power]) + self.io = self.Export(self.res.b.adapt_to(DigitalSource.pullup_from_supply(self.pwr)), [InOut]) + + def connected( + self, pwr: Optional[Port[VoltageLink]] = None, io: Optional[Port[DigitalLink]] = None + ) -> "PullupResistor": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if pwr is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr, self.pwr) + if io is not None: + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class PulldownResistor(DiscreteApplication): - """Pull-down resistor with an VoltageSink for automatic implicit connect to a Ground line.""" - def __init__(self, resistance: RangeLike) -> None: - super().__init__() + """Pull-down resistor with an VoltageSink for automatic implicit connect to a Ground line.""" + + def __init__(self, resistance: RangeLike) -> None: + super().__init__() - self.res = self.Block(Resistor(resistance, 0*Watt(tol=0))) # TODO automatically calculate power + self.res = self.Block(Resistor(resistance, 0 * Watt(tol=0))) # TODO automatically calculate power - self.gnd = self.Export(self.res.a.adapt_to(Ground()), [Common]) - self.io = self.Export(self.res.b.adapt_to( - DigitalSource.pulldown_from_supply(self.gnd) - ), [InOut]) + self.gnd = self.Export(self.res.a.adapt_to(Ground()), [Common]) + self.io = self.Export(self.res.b.adapt_to(DigitalSource.pulldown_from_supply(self.gnd)), [InOut]) - def connected(self, gnd: Optional[Port[GroundLink]] = None, io: Optional[Port[DigitalLink]] = None) -> \ - 'PulldownResistor': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if gnd is not None: - cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) - if io is not None: - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + def connected( + self, gnd: Optional[Port[GroundLink]] = None, io: Optional[Port[DigitalLink]] = None + ) -> "PulldownResistor": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if gnd is not None: + cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) + if io is not None: + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class PullupResistorArray(TypedTestPoint, GeneratorBlock): - """Array of PullupResistors, sized from the port array's connections.""" - def __init__(self, resistance: RangeLike): - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.io = self.Port(Vector(DigitalSource.empty()), [InOut]) - self.generator_param(self.io.requested()) - self.resistance = self.ArgParameter(resistance) - - @override - def generate(self) -> None: - super().generate() - self.res = ElementDict[PullupResistor]() - for requested in self.get(self.io.requested()): - res = self.res[requested] = self.Block(PullupResistor(self.resistance)) - self.connect(self.pwr, res.pwr) - self.connect(self.io.append_elt(DigitalSource.empty(), requested), res.io) + """Array of PullupResistors, sized from the port array's connections.""" + + def __init__(self, resistance: RangeLike): + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.io = self.Port(Vector(DigitalSource.empty()), [InOut]) + self.generator_param(self.io.requested()) + self.resistance = self.ArgParameter(resistance) + + @override + def generate(self) -> None: + super().generate() + self.res = ElementDict[PullupResistor]() + for requested in self.get(self.io.requested()): + res = self.res[requested] = self.Block(PullupResistor(self.resistance)) + self.connect(self.pwr, res.pwr) + self.connect(self.io.append_elt(DigitalSource.empty(), requested), res.io) class PulldownResistorArray(TypedTestPoint, GeneratorBlock): - """Array of PulldownResistors, sized from the port array's connections.""" - def __init__(self, resistance: RangeLike): - super().__init__() - self.gnd = self.Port(Ground.empty(), [Common]) - self.io = self.Port(Vector(DigitalSource.empty()), [InOut]) - self.generator_param(self.io.requested()) - self.resistance = self.ArgParameter(resistance) - - @override - def generate(self) -> None: - super().generate() - self.res = ElementDict[PulldownResistor]() - for requested in self.get(self.io.requested()): - res = self.res[requested] = self.Block(PulldownResistor(self.resistance)) - self.connect(self.gnd, res.gnd) - self.connect(self.io.append_elt(DigitalSource.empty(), requested), res.io) + """Array of PulldownResistors, sized from the port array's connections.""" + + def __init__(self, resistance: RangeLike): + super().__init__() + self.gnd = self.Port(Ground.empty(), [Common]) + self.io = self.Port(Vector(DigitalSource.empty()), [InOut]) + self.generator_param(self.io.requested()) + self.resistance = self.ArgParameter(resistance) + + @override + def generate(self) -> None: + super().generate() + self.res = ElementDict[PulldownResistor]() + for requested in self.get(self.io.requested()): + res = self.res[requested] = self.Block(PulldownResistor(self.resistance)) + self.connect(self.gnd, res.gnd) + self.connect(self.io.append_elt(DigitalSource.empty(), requested), res.io) class SeriesPowerResistor(DiscreteApplication, KiCadImportableBlock): - """Series resistor for power applications""" - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name in ('Device:R', 'Device:R_Small') - return {'1': self.pwr_in, '2': self.pwr_out} - - def __init__(self, resistance: RangeLike) -> None: - super().__init__() - - self.resistance = self.ArgParameter(resistance) - - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration - self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration - current_draw = self.pwr_out.link().current_drawn.abs() - - self.res = self.Block(Resistor( - resistance=self.resistance, - power=current_draw * current_draw * self.resistance - )) - - self.connect(self.pwr_in, self.res.a.adapt_to(VoltageSink( - current_draw=self.pwr_out.link().current_drawn - ))) - self.connect(self.pwr_out, self.res.b.adapt_to(VoltageSource( - voltage_out=self.pwr_in.link().voltage, # ignore voltage drop - ))) - - self.actual_power = self.Parameter(RangeExpr(current_draw * current_draw * self.res.actual_resistance)) - self.require(self.actual_power.within(self.res.actual_power_rating)) - self.actual_resistance = self.Parameter(RangeExpr(self.res.actual_resistance)) - - def connected(self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None) -> \ - 'SeriesPowerResistor': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if pwr_in is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) - if pwr_out is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) - return self + """Series resistor for power applications""" + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name in ("Device:R", "Device:R_Small") + return {"1": self.pwr_in, "2": self.pwr_out} + + def __init__(self, resistance: RangeLike) -> None: + super().__init__() + + self.resistance = self.ArgParameter(resistance) + + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration + self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration + current_draw = self.pwr_out.link().current_drawn.abs() + + self.res = self.Block(Resistor(resistance=self.resistance, power=current_draw * current_draw * self.resistance)) + + self.connect(self.pwr_in, self.res.a.adapt_to(VoltageSink(current_draw=self.pwr_out.link().current_drawn))) + self.connect( + self.pwr_out, + self.res.b.adapt_to( + VoltageSource( + voltage_out=self.pwr_in.link().voltage, # ignore voltage drop + ) + ), + ) + + self.actual_power = self.Parameter(RangeExpr(current_draw * current_draw * self.res.actual_resistance)) + self.require(self.actual_power.within(self.res.actual_power_rating)) + self.actual_resistance = self.Parameter(RangeExpr(self.res.actual_resistance)) + + def connected( + self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None + ) -> "SeriesPowerResistor": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if pwr_in is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) + if pwr_out is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) + return self class CurrentSenseResistor(DiscreteApplication, KiCadImportableBlock, GeneratorBlock): - """Current sense resistor with a power passthrough resistor and positive and negative sense temrinals.""" - def __init__(self, resistance: RangeLike, sense_in_reqd: BoolLike = True) -> None: - super().__init__() + """Current sense resistor with a power passthrough resistor and positive and negative sense temrinals.""" + + def __init__(self, resistance: RangeLike, sense_in_reqd: BoolLike = True) -> None: + super().__init__() - self.res = self.Block(SeriesPowerResistor(resistance)) - self.pwr_in = self.Export(self.res.pwr_in, [Input]) - self.pwr_out = self.Export(self.res.pwr_out, [Output]) + self.res = self.Block(SeriesPowerResistor(resistance)) + self.pwr_in = self.Export(self.res.pwr_in, [Input]) + self.pwr_out = self.Export(self.res.pwr_out, [Output]) - self.sense_in = self.Port(AnalogSource.empty(), optional=True) - self.sense_out = self.Port(AnalogSource.empty()) + self.sense_in = self.Port(AnalogSource.empty(), optional=True) + self.sense_out = self.Port(AnalogSource.empty()) - # in some cases, the input rail may be the sense reference and this connection is optional - # but this must be an explicit opt-in - sense_in_reqd_param = self.ArgParameter(sense_in_reqd) - self.require(sense_in_reqd_param.implies(self.sense_in.is_connected())) + # in some cases, the input rail may be the sense reference and this connection is optional + # but this must be an explicit opt-in + sense_in_reqd_param = self.ArgParameter(sense_in_reqd) + self.require(sense_in_reqd_param.implies(self.sense_in.is_connected())) - self.generator_param(self.sense_in.is_connected()) + self.generator_param(self.sense_in.is_connected()) - self.actual_resistance = self.Parameter(RangeExpr(self.res.actual_resistance)) + self.actual_resistance = self.Parameter(RangeExpr(self.res.actual_resistance)) - @override - def generate(self) -> None: - super().generate() + @override + def generate(self) -> None: + super().generate() - if self.get(self.sense_in.is_connected()): - self.connect(self.pwr_in.as_analog_source(), self.sense_in) - self.connect(self.res.pwr_out.as_analog_source(), self.sense_out) + if self.get(self.sense_in.is_connected()): + self.connect(self.pwr_in.as_analog_source(), self.sense_in) + self.connect(self.res.pwr_out.as_analog_source(), self.sense_out) - def connected(self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None) -> \ - 'CurrentSenseResistor': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if pwr_in is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) - if pwr_out is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) - return self + def connected( + self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optional[Port[VoltageLink]] = None + ) -> "CurrentSenseResistor": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if pwr_in is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_in) + if pwr_out is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out) + return self - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: - assert symbol_name == 'edg_importable:CurrentSenseResistor' - return {'1': self.pwr_in, '2': self.pwr_out, 'sense_in': self.sense_in, 'sense_out': self.sense_out} + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: + assert symbol_name == "edg_importable:CurrentSenseResistor" + return {"1": self.pwr_in, "2": self.pwr_out, "sense_in": self.sense_in, "sense_out": self.sense_out} class AnalogClampResistor(Protection, KiCadImportableBlock): - """Inline resistor that limits the current (to a parameterized amount) which works in concert - with ESD diodes in the downstream device to clamp the signal voltage to allowable levels. - - The protection voltage can be extended beyond the modeled range from the input signal, - and can also be specified to allow zero output voltage (for when the downstream device - is powered down) - - TODO: clamp_target should be inferred from the target voltage_limits, - but voltage_limits doesn't always get propagated""" - def __init__(self, clamp_target: RangeLike = (0, 3)*Volt, clamp_current: RangeLike = (0.25, 2.5)*mAmp, - protection_voltage: RangeLike = (0, 0)*Volt, zero_out: BoolLike = False): - super().__init__() - - self.signal_in = self.Port(AnalogSink.empty(), [Input]) - self.signal_out = self.Port(AnalogSource.empty(), [Output]) - - self.clamp_target = self.ArgParameter(clamp_target) - self.clamp_current = self.ArgParameter(clamp_current) - self.protection_voltage = self.ArgParameter(protection_voltage) - self.zero_out = self.ArgParameter(zero_out) - - @override - def contents(self) -> None: - super().contents() - - # TODO bidirectional clamping calcs? - self.res = self.Block(Resistor(resistance=1/self.clamp_current * self.zero_out.then_else( - self.signal_in.link().voltage.hull(self.protection_voltage).upper(), - self.signal_in.link().voltage.hull(self.protection_voltage).upper() - self.clamp_target.upper(), - ))) - self.connect(self.res.a.adapt_to(AnalogSink()), self.signal_in) - self.connect(self.res.b.adapt_to(AnalogSource( - voltage_out=self.signal_in.link().voltage.intersect(self.clamp_target), - signal_out=self.signal_in.link().signal, - impedance=self.signal_in.link().source_impedance + self.res.actual_resistance - )), self.signal_out) - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: - assert symbol_name == 'Device:R' - return {'1': self.signal_in, '2': self.signal_out} + """Inline resistor that limits the current (to a parameterized amount) which works in concert + with ESD diodes in the downstream device to clamp the signal voltage to allowable levels. + + The protection voltage can be extended beyond the modeled range from the input signal, + and can also be specified to allow zero output voltage (for when the downstream device + is powered down) + + TODO: clamp_target should be inferred from the target voltage_limits, + but voltage_limits doesn't always get propagated""" + + def __init__( + self, + clamp_target: RangeLike = (0, 3) * Volt, + clamp_current: RangeLike = (0.25, 2.5) * mAmp, + protection_voltage: RangeLike = (0, 0) * Volt, + zero_out: BoolLike = False, + ): + super().__init__() + + self.signal_in = self.Port(AnalogSink.empty(), [Input]) + self.signal_out = self.Port(AnalogSource.empty(), [Output]) + + self.clamp_target = self.ArgParameter(clamp_target) + self.clamp_current = self.ArgParameter(clamp_current) + self.protection_voltage = self.ArgParameter(protection_voltage) + self.zero_out = self.ArgParameter(zero_out) + + @override + def contents(self) -> None: + super().contents() + + # TODO bidirectional clamping calcs? + self.res = self.Block( + Resistor( + resistance=1 + / self.clamp_current + * self.zero_out.then_else( + self.signal_in.link().voltage.hull(self.protection_voltage).upper(), + self.signal_in.link().voltage.hull(self.protection_voltage).upper() - self.clamp_target.upper(), + ) + ) + ) + self.connect(self.res.a.adapt_to(AnalogSink()), self.signal_in) + self.connect( + self.res.b.adapt_to( + AnalogSource( + voltage_out=self.signal_in.link().voltage.intersect(self.clamp_target), + signal_out=self.signal_in.link().signal, + impedance=self.signal_in.link().source_impedance + self.res.actual_resistance, + ) + ), + self.signal_out, + ) + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: + assert symbol_name == "Device:R" + return {"1": self.signal_in, "2": self.signal_out} class DigitalClampResistor(Protection, KiCadImportableBlock): - """Inline resistor that limits the current (to a parameterized amount) which works in concert - with ESD diodes in the downstream device to clamp the signal voltage to allowable levels. - - The protection voltage can be extended beyond the modeled range from the input signal, - and can also be specified to allow zero output voltage (for when the downstream device - is powered down) - - TODO: clamp_target should be inferred from the target voltage_limits, - but voltage_limits doesn't always get propagated.""" - def __init__(self, clamp_target: RangeLike = (0, 3)*Volt, clamp_current: RangeLike = (1.0, 10)*mAmp, - protection_voltage: RangeLike = (0, 0)*Volt, zero_out: BoolLike = False): - super().__init__() - - self.signal_in = self.Port(DigitalSink.empty(), [Input]) - self.signal_out = self.Port(DigitalSource.empty(), [Output]) - - self.clamp_target = self.ArgParameter(clamp_target) - self.clamp_current = self.ArgParameter(clamp_current) - self.protection_voltage = self.ArgParameter(protection_voltage) - self.zero_out = self.ArgParameter(zero_out) - - @override - def contents(self) -> None: - super().contents() - - # TODO bidirectional clamping calcs? - self.res = self.Block(Resistor(resistance=1/self.clamp_current * self.zero_out.then_else( - self.signal_in.link().voltage.hull(self.protection_voltage).upper(), - self.signal_in.link().voltage.hull(self.protection_voltage).upper() - self.clamp_target.upper(), - ))) - self.connect(self.res.a.adapt_to(DigitalSink(current_draw=self.signal_out.link().current_drawn)), self.signal_in) - self.connect(self.res.b.adapt_to(DigitalSource( - voltage_out=self.signal_in.link().voltage.intersect(self.clamp_target), - output_thresholds=self.signal_in.link().output_thresholds - )), self.signal_out) - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: - assert symbol_name == 'Device:R' - return {'1': self.signal_in, '2': self.signal_out} + """Inline resistor that limits the current (to a parameterized amount) which works in concert + with ESD diodes in the downstream device to clamp the signal voltage to allowable levels. + + The protection voltage can be extended beyond the modeled range from the input signal, + and can also be specified to allow zero output voltage (for when the downstream device + is powered down) + + TODO: clamp_target should be inferred from the target voltage_limits, + but voltage_limits doesn't always get propagated.""" + + def __init__( + self, + clamp_target: RangeLike = (0, 3) * Volt, + clamp_current: RangeLike = (1.0, 10) * mAmp, + protection_voltage: RangeLike = (0, 0) * Volt, + zero_out: BoolLike = False, + ): + super().__init__() + + self.signal_in = self.Port(DigitalSink.empty(), [Input]) + self.signal_out = self.Port(DigitalSource.empty(), [Output]) + + self.clamp_target = self.ArgParameter(clamp_target) + self.clamp_current = self.ArgParameter(clamp_current) + self.protection_voltage = self.ArgParameter(protection_voltage) + self.zero_out = self.ArgParameter(zero_out) + + @override + def contents(self) -> None: + super().contents() + + # TODO bidirectional clamping calcs? + self.res = self.Block( + Resistor( + resistance=1 + / self.clamp_current + * self.zero_out.then_else( + self.signal_in.link().voltage.hull(self.protection_voltage).upper(), + self.signal_in.link().voltage.hull(self.protection_voltage).upper() - self.clamp_target.upper(), + ) + ) + ) + self.connect( + self.res.a.adapt_to(DigitalSink(current_draw=self.signal_out.link().current_drawn)), self.signal_in + ) + self.connect( + self.res.b.adapt_to( + DigitalSource( + voltage_out=self.signal_in.link().voltage.intersect(self.clamp_target), + output_thresholds=self.signal_in.link().output_thresholds, + ) + ), + self.signal_out, + ) + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: + assert symbol_name == "Device:R" + return {"1": self.signal_in, "2": self.signal_out} diff --git a/edg/abstract_parts/AbstractResistorArray.py b/edg/abstract_parts/AbstractResistorArray.py index bdfed3bb0..bf515a89f 100644 --- a/edg/abstract_parts/AbstractResistorArray.py +++ b/edg/abstract_parts/AbstractResistorArray.py @@ -11,121 +11,134 @@ class ResistorArrayElement(Resistor): # to avoid an abstract part error - def __init__(self) -> None: - super().__init__(resistance=RangeExpr(), power=RangeExpr()) + def __init__(self) -> None: + super().__init__(resistance=RangeExpr(), power=RangeExpr()) @abstract_block class ResistorArray(MultipackDevice, MultipackBlock, HasStandardFootprint): - """An n-element resistor array, where all resistors have the same resistance and power rating.""" - _STANDARD_FOOTPRINT = lambda: ResistorArrayStandardFootprint - - def __init__(self, count: IntLike = 0) -> None: # 0 means 'size automatically' - super().__init__() - - self.count = self.ArgParameter(count) - - self.elements = self.PackedPart(PackedBlockArray(ResistorArrayElement())) - self.a = self.PackedExport(self.elements.ports_array(lambda x: x.a)) - self.b = self.PackedExport(self.elements.ports_array(lambda x: x.b)) - self.resistances = self.PackedParameter(self.elements.params_array(lambda x: x.resistance)) - self.powers = self.PackedParameter(self.elements.params_array(lambda x: x.power)) - - self.actual_count = self.Parameter(IntExpr()) - self.actual_resistance = self.Parameter(RangeExpr()) - self.actual_power_rating = self.Parameter(RangeExpr()) # per element - - self.unpacked_assign(self.elements.params(lambda x: x.actual_resistance), self.actual_resistance) - self.unpacked_assign(self.elements.params(lambda x: x.actual_power_rating), self.actual_power_rating) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( # TODO better support for array typed - "count: ", DescriptionString.FormatUnits(self.actual_count, ""), # TODO unitless - " of spec ", DescriptionString.FormatUnits(self.count, ""), "\n", - "resistance: ", DescriptionString.FormatUnits(self.actual_resistance, "Ω"), - " of specs ", DescriptionString.FormatUnits(self.resistances, "Ω"), "\n", - "element power: ", DescriptionString.FormatUnits(self.actual_power_rating, "W"), - " of operating: ", DescriptionString.FormatUnits(self.powers, "W") - ) + """An n-element resistor array, where all resistors have the same resistance and power rating.""" + + _STANDARD_FOOTPRINT = lambda: ResistorArrayStandardFootprint + + def __init__(self, count: IntLike = 0) -> None: # 0 means 'size automatically' + super().__init__() + + self.count = self.ArgParameter(count) + + self.elements = self.PackedPart(PackedBlockArray(ResistorArrayElement())) + self.a = self.PackedExport(self.elements.ports_array(lambda x: x.a)) + self.b = self.PackedExport(self.elements.ports_array(lambda x: x.b)) + self.resistances = self.PackedParameter(self.elements.params_array(lambda x: x.resistance)) + self.powers = self.PackedParameter(self.elements.params_array(lambda x: x.power)) + + self.actual_count = self.Parameter(IntExpr()) + self.actual_resistance = self.Parameter(RangeExpr()) + self.actual_power_rating = self.Parameter(RangeExpr()) # per element + + self.unpacked_assign(self.elements.params(lambda x: x.actual_resistance), self.actual_resistance) + self.unpacked_assign(self.elements.params(lambda x: x.actual_power_rating), self.actual_power_rating) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( # TODO better support for array typed + "count: ", + DescriptionString.FormatUnits(self.actual_count, ""), # TODO unitless + " of spec ", + DescriptionString.FormatUnits(self.count, ""), + "\n", + "resistance: ", + DescriptionString.FormatUnits(self.actual_resistance, "Ω"), + " of specs ", + DescriptionString.FormatUnits(self.resistances, "Ω"), + "\n", + "element power: ", + DescriptionString.FormatUnits(self.actual_power_rating, "W"), + " of operating: ", + DescriptionString.FormatUnits(self.powers, "W"), + ) class ResistorArrayStandardFootprint(StandardFootprint[ResistorArray]): - REFDES_PREFIX = 'RN' - - # TODO some way to ensure the resistor count is sufficient? - FOOTPRINT_PINNING_MAP = { # these are all the footprints in KiCad as of 2022 05 31 - ( - 'Resistor_SMD:R_Array_Concave_2x0603', - 'Resistor_SMD:R_Array_Convex_2x0402', - 'Resistor_SMD:R_Array_Convex_2x0603', - 'Resistor_SMD:R_Array_Convex_2x0606', - 'Resistor_SMD:R_Array_Convex_2x1206', - ): lambda block: { - '1': block.a['0'], - '4': block.b['0'], - '2': block.a['1'], - '3': block.b['1'], - }, - ( - 'Resistor_SMD:R_Array_Concave_4x0402', - 'Resistor_SMD:R_Array_Concave_4x0603', - 'Resistor_SMD:R_Array_Convex_4x0402', - 'Resistor_SMD:R_Array_Convex_4x0603', - 'Resistor_SMD:R_Array_Convex_4x0612', - 'Resistor_SMD:R_Array_Convex_4x1206', - ): lambda block: { - '1': block.a['0'], - '8': block.b['0'], - '2': block.a['1'], - '7': block.b['1'], - '3': block.a['2'], - '6': block.b['2'], - '4': block.a['3'], - '5': block.b['3'], - }, - } + REFDES_PREFIX = "RN" + + # TODO some way to ensure the resistor count is sufficient? + FOOTPRINT_PINNING_MAP = { # these are all the footprints in KiCad as of 2022 05 31 + ( + "Resistor_SMD:R_Array_Concave_2x0603", + "Resistor_SMD:R_Array_Convex_2x0402", + "Resistor_SMD:R_Array_Convex_2x0603", + "Resistor_SMD:R_Array_Convex_2x0606", + "Resistor_SMD:R_Array_Convex_2x1206", + ): lambda block: { + "1": block.a["0"], + "4": block.b["0"], + "2": block.a["1"], + "3": block.b["1"], + }, + ( + "Resistor_SMD:R_Array_Concave_4x0402", + "Resistor_SMD:R_Array_Concave_4x0603", + "Resistor_SMD:R_Array_Convex_4x0402", + "Resistor_SMD:R_Array_Convex_4x0603", + "Resistor_SMD:R_Array_Convex_4x0612", + "Resistor_SMD:R_Array_Convex_4x1206", + ): lambda block: { + "1": block.a["0"], + "8": block.b["0"], + "2": block.a["1"], + "7": block.b["1"], + "3": block.a["2"], + "6": block.b["2"], + "4": block.a["3"], + "5": block.b["3"], + }, + } @non_library class TableResistorArray(PartsTableSelector, ResistorArray): - RESISTANCE = PartsTableColumn(Range) - POWER_RATING = PartsTableColumn(Range) - COUNT = PartsTableColumn(int) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.count, self.a.requested(), self.b.requested(), self.resistances, self.powers) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - # TODO some kind of range intersect construct? - resistances_min = max([resistance.lower for resistance in self.get(self.resistances)]) - resistances_max = min([resistance.upper for resistance in self.get(self.resistances)]) - assert resistances_min <= resistances_max, "resistances do not intersect" - resistance_intersect = Range(resistances_min, resistances_max) - - powers_min = min([power.lower for power in self.get(self.powers)]) - powers_max = max([power.upper for power in self.get(self.powers)]) - powers_hull = Range(powers_min, powers_max) - - return super()._row_filter(row) and \ - (self.get(self.count) == 0 or self.get(self.count) == row[self.COUNT]) and \ - (row[self.COUNT] >= len(self.get(self.a.requested())) and \ - row[self.COUNT] >= len(self.get(self.b.requested()))) and \ - row[self.RESISTANCE].fuzzy_in(resistance_intersect) and \ - powers_hull.fuzzy_in(row[self.POWER_RATING]) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - for i in range(row[self.COUNT]): # must generate ports before creating the footprint - self.a.append_elt(Passive(), str(i)) - self.b.append_elt(Passive(), str(i)) - - super()._row_generate(row) - - self.assign(self.actual_count, row[self.COUNT]) - self.assign(self.actual_resistance, row[self.RESISTANCE]) - self.assign(self.actual_power_rating, row[self.POWER_RATING]) + RESISTANCE = PartsTableColumn(Range) + POWER_RATING = PartsTableColumn(Range) + COUNT = PartsTableColumn(int) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.count, self.a.requested(), self.b.requested(), self.resistances, self.powers) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + # TODO some kind of range intersect construct? + resistances_min = max([resistance.lower for resistance in self.get(self.resistances)]) + resistances_max = min([resistance.upper for resistance in self.get(self.resistances)]) + assert resistances_min <= resistances_max, "resistances do not intersect" + resistance_intersect = Range(resistances_min, resistances_max) + + powers_min = min([power.lower for power in self.get(self.powers)]) + powers_max = max([power.upper for power in self.get(self.powers)]) + powers_hull = Range(powers_min, powers_max) + + return ( + super()._row_filter(row) + and (self.get(self.count) == 0 or self.get(self.count) == row[self.COUNT]) + and ( + row[self.COUNT] >= len(self.get(self.a.requested())) + and row[self.COUNT] >= len(self.get(self.b.requested())) + ) + and row[self.RESISTANCE].fuzzy_in(resistance_intersect) + and powers_hull.fuzzy_in(row[self.POWER_RATING]) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + for i in range(row[self.COUNT]): # must generate ports before creating the footprint + self.a.append_elt(Passive(), str(i)) + self.b.append_elt(Passive(), str(i)) + + super()._row_generate(row) + + self.assign(self.actual_count, row[self.COUNT]) + self.assign(self.actual_resistance, row[self.RESISTANCE]) + self.assign(self.actual_power_rating, row[self.POWER_RATING]) diff --git a/edg/abstract_parts/AbstractSolidStateRelay.py b/edg/abstract_parts/AbstractSolidStateRelay.py index 6654d78e8..f146e2439 100644 --- a/edg/abstract_parts/AbstractSolidStateRelay.py +++ b/edg/abstract_parts/AbstractSolidStateRelay.py @@ -10,118 +10,148 @@ @abstract_block class SolidStateRelay(Interface, Block): - """Base class for solid state relays. - LED pins are passive (like the abstract LED) and the enclosing class should provide - the circuitry to make it a DigitalSink port. - """ - def __init__(self) -> None: - super().__init__() + """Base class for solid state relays. + LED pins are passive (like the abstract LED) and the enclosing class should provide + the circuitry to make it a DigitalSink port. + """ - self.leda = self.Port(Passive.empty()) - self.ledk = self.Port(Passive.empty()) + def __init__(self) -> None: + super().__init__() - self.feta = self.Port(Passive.empty()) - self.fetb = self.Port(Passive.empty()) + self.leda = self.Port(Passive.empty()) + self.ledk = self.Port(Passive.empty()) - # TODO: this is a different way of modeling parts - parameters in the part itself - # instead of on the ports (because this doesn't have typed ports) - self.led_forward_voltage = self.Parameter(RangeExpr()) - self.led_current_limit = self.Parameter(RangeExpr()) - self.led_current_recommendation = self.Parameter(RangeExpr()) - self.load_voltage_limit = self.Parameter(RangeExpr()) - self.load_current_limit = self.Parameter(RangeExpr()) - self.load_resistance = self.Parameter(RangeExpr()) + self.feta = self.Port(Passive.empty()) + self.fetb = self.Port(Passive.empty()) + + # TODO: this is a different way of modeling parts - parameters in the part itself + # instead of on the ports (because this doesn't have typed ports) + self.led_forward_voltage = self.Parameter(RangeExpr()) + self.led_current_limit = self.Parameter(RangeExpr()) + self.led_current_recommendation = self.Parameter(RangeExpr()) + self.load_voltage_limit = self.Parameter(RangeExpr()) + self.load_current_limit = self.Parameter(RangeExpr()) + self.load_resistance = self.Parameter(RangeExpr()) class VoltageIsolatedSwitch(Interface, KiCadImportableBlock, Block): - """Digitally controlled solid state relay that switches a voltage signal. - Includes a ballasting resistor. - - The ports are not tagged with Input/Output/InOut, because of potential for confusion between - the digital side and the analog side. - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'edg_importable:VoltageIsolatedSwitch' - return {'in': self.signal, 'gnd': self.gnd, 'ain': self.pwr_in, 'aout': self.pwr_out} - - def __init__(self) -> None: - super().__init__() - - self.signal = self.Port(DigitalSink.empty()) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.pwr_in = self.Port(VoltageSink.empty()) - self.pwr_out = self.Port(VoltageSource.empty()) - - self.ic = self.Block(SolidStateRelay()) - self.res = self.Block(Resistor( - resistance=(self.signal.link().voltage.upper() / self.ic.led_current_recommendation.upper(), - self.signal.link().output_thresholds.upper() / self.ic.led_current_recommendation.lower()) - )) - self.connect(self.signal, self.ic.leda.adapt_to(DigitalSink( - current_draw=self.signal.link().voltage / self.res.actual_resistance - ))) - self.connect(self.res.a, self.ic.ledk) - self.connect(self.res.b.adapt_to(Ground()), self.gnd) - - self.connect(self.pwr_in, self.ic.feta.adapt_to(VoltageSink( - voltage_limits=self.ic.load_voltage_limit, # TODO: assumed magic ground - current_draw=self.pwr_out.link().current_drawn - ))) - self.connect(self.pwr_out, self.ic.fetb.adapt_to(VoltageSource( - voltage_out=self.pwr_in.link().voltage, - current_limits=self.ic.load_current_limit, - ))) + """Digitally controlled solid state relay that switches a voltage signal. + Includes a ballasting resistor. + + The ports are not tagged with Input/Output/InOut, because of potential for confusion between + the digital side and the analog side. + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name == "edg_importable:VoltageIsolatedSwitch" + return {"in": self.signal, "gnd": self.gnd, "ain": self.pwr_in, "aout": self.pwr_out} + + def __init__(self) -> None: + super().__init__() + + self.signal = self.Port(DigitalSink.empty()) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.pwr_in = self.Port(VoltageSink.empty()) + self.pwr_out = self.Port(VoltageSource.empty()) + + self.ic = self.Block(SolidStateRelay()) + self.res = self.Block( + Resistor( + resistance=( + self.signal.link().voltage.upper() / self.ic.led_current_recommendation.upper(), + self.signal.link().output_thresholds.upper() / self.ic.led_current_recommendation.lower(), + ) + ) + ) + self.connect( + self.signal, + self.ic.leda.adapt_to(DigitalSink(current_draw=self.signal.link().voltage / self.res.actual_resistance)), + ) + self.connect(self.res.a, self.ic.ledk) + self.connect(self.res.b.adapt_to(Ground()), self.gnd) + + self.connect( + self.pwr_in, + self.ic.feta.adapt_to( + VoltageSink( + voltage_limits=self.ic.load_voltage_limit, # TODO: assumed magic ground + current_draw=self.pwr_out.link().current_drawn, + ) + ), + ) + self.connect( + self.pwr_out, + self.ic.fetb.adapt_to( + VoltageSource( + voltage_out=self.pwr_in.link().voltage, + current_limits=self.ic.load_current_limit, + ) + ), + ) class AnalogIsolatedSwitch(Interface, KiCadImportableBlock, Block): - """Digitally controlled solid state relay that switches an analog signal. - Includes a ballasting resistor. - - The ports are not tagged with Input/Output/InOut, because of potential for confusion between - the digital side and the analog side. - - A separate output-side pull port allows modeling the output switch standoff voltage - when the switch is off. - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'edg_importable:AnalogIsolatedSwitch' - return {'in': self.signal, 'gnd': self.gnd, - 'ain': self.ain, 'apull': self.apull, 'aout': self.aout} - - def __init__(self) -> None: - super().__init__() - - self.signal = self.Port(DigitalSink.empty()) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.apull = self.Port(AnalogSink.empty()) - self.ain = self.Port(AnalogSink.empty()) - self.aout = self.Port(AnalogSource.empty()) - - self.ic = self.Block(SolidStateRelay()) - self.res = self.Block(Resistor( - resistance=(self.signal.link().voltage.upper() / self.ic.led_current_recommendation.upper(), - self.signal.link().output_thresholds.upper() / self.ic.led_current_recommendation.lower()) - )) - self.connect(self.signal, self.ic.leda.adapt_to(DigitalSink( - current_draw=self.signal.link().voltage / self.res.actual_resistance - ))) - self.connect(self.res.a, self.ic.ledk) - self.connect(self.res.b.adapt_to(Ground()), self.gnd) - - self.connect(self.ain, self.ic.feta.adapt_to(AnalogSink( - voltage_limits=self.apull.link().voltage + self.ic.load_voltage_limit, - impedance=self.aout.link().sink_impedance + self.ic.load_resistance - ))) - self.pull_merge = self.Block(MergedAnalogSource()).connected_from( - self.apull, - self.ic.fetb.adapt_to(AnalogSource( - voltage_out=self.ain.link().voltage, - signal_out=self.ain.link().signal, - current_limits=self.ic.load_current_limit, - impedance=self.ain.link().source_impedance + self.ic.load_resistance - ))) - self.connect(self.pull_merge.output, self.aout) + """Digitally controlled solid state relay that switches an analog signal. + Includes a ballasting resistor. + + The ports are not tagged with Input/Output/InOut, because of potential for confusion between + the digital side and the analog side. + + A separate output-side pull port allows modeling the output switch standoff voltage + when the switch is off. + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name == "edg_importable:AnalogIsolatedSwitch" + return {"in": self.signal, "gnd": self.gnd, "ain": self.ain, "apull": self.apull, "aout": self.aout} + + def __init__(self) -> None: + super().__init__() + + self.signal = self.Port(DigitalSink.empty()) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.apull = self.Port(AnalogSink.empty()) + self.ain = self.Port(AnalogSink.empty()) + self.aout = self.Port(AnalogSource.empty()) + + self.ic = self.Block(SolidStateRelay()) + self.res = self.Block( + Resistor( + resistance=( + self.signal.link().voltage.upper() / self.ic.led_current_recommendation.upper(), + self.signal.link().output_thresholds.upper() / self.ic.led_current_recommendation.lower(), + ) + ) + ) + self.connect( + self.signal, + self.ic.leda.adapt_to(DigitalSink(current_draw=self.signal.link().voltage / self.res.actual_resistance)), + ) + self.connect(self.res.a, self.ic.ledk) + self.connect(self.res.b.adapt_to(Ground()), self.gnd) + + self.connect( + self.ain, + self.ic.feta.adapt_to( + AnalogSink( + voltage_limits=self.apull.link().voltage + self.ic.load_voltage_limit, + impedance=self.aout.link().sink_impedance + self.ic.load_resistance, + ) + ), + ) + self.pull_merge = self.Block(MergedAnalogSource()).connected_from( + self.apull, + self.ic.fetb.adapt_to( + AnalogSource( + voltage_out=self.ain.link().voltage, + signal_out=self.ain.link().signal, + current_limits=self.ic.load_current_limit, + impedance=self.ain.link().source_impedance + self.ic.load_resistance, + ) + ), + ) + self.connect(self.pull_merge.output, self.aout) diff --git a/edg/abstract_parts/AbstractSpiMemory.py b/edg/abstract_parts/AbstractSpiMemory.py index 975fae1df..6250ef9ff 100644 --- a/edg/abstract_parts/AbstractSpiMemory.py +++ b/edg/abstract_parts/AbstractSpiMemory.py @@ -6,27 +6,29 @@ @abstract_block class SpiMemory(Memory, Block): - """Base class for SPI memory, with acceptable sizes (in bits) as a range.""" - def __init__(self, size: RangeLike) -> None: - super().__init__() + """Base class for SPI memory, with acceptable sizes (in bits) as a range.""" - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) + def __init__(self, size: RangeLike) -> None: + super().__init__() - self.spi = self.Port(SpiPeripheral.empty()) - self.cs = self.Port(DigitalSink.empty()) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.size = self.ArgParameter(size) - self.actual_size = self.Parameter(IntExpr()) + self.spi = self.Port(SpiPeripheral.empty()) + self.cs = self.Port(DigitalSink.empty()) + + self.size = self.ArgParameter(size) + self.actual_size = self.Parameter(IntExpr()) @abstract_block class SpiMemoryQspi(BlockInterfaceMixin[SpiMemory]): - """SPI memory that also supports QSPI mode (4-line SPI). - Vanilla SPI SDI maps to IO0, and SDO maps to IO1. - EXPERIMENTAL - interface subject to change. - May prevent the use of some chip functions that conflict with QSPI lines.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.io2 = self.Port(DigitalBidir.empty(), optional=True) - self.io3 = self.Port(DigitalBidir.empty(), optional=True) + """SPI memory that also supports QSPI mode (4-line SPI). + Vanilla SPI SDI maps to IO0, and SDO maps to IO1. + EXPERIMENTAL - interface subject to change. + May prevent the use of some chip functions that conflict with QSPI lines.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.io2 = self.Port(DigitalBidir.empty(), optional=True) + self.io3 = self.Port(DigitalBidir.empty(), optional=True) diff --git a/edg/abstract_parts/AbstractSwitch.py b/edg/abstract_parts/AbstractSwitch.py index eaacab234..c91b5eeb2 100644 --- a/edg/abstract_parts/AbstractSwitch.py +++ b/edg/abstract_parts/AbstractSwitch.py @@ -8,197 +8,217 @@ @abstract_block class Switch(KiCadImportableBlock, DiscreteComponent): - """Two-ported device that closes a circuit when pressed.""" - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'Switch:SW_SPST' - return {'1': self.sw, '2': self.com} + """Two-ported device that closes a circuit when pressed.""" - def __init__(self, voltage: RangeLike, current: RangeLike = 0*Amp(tol=0)) -> None: - super().__init__() + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name == "Switch:SW_SPST" + return {"1": self.sw, "2": self.com} - self.sw = self.Port(Passive.empty()) - self.com = self.Port(Passive.empty()) + def __init__(self, voltage: RangeLike, current: RangeLike = 0 * Amp(tol=0)) -> None: + super().__init__() - self.current = self.ArgParameter(current) - self.voltage = self.ArgParameter(voltage) + self.sw = self.Port(Passive.empty()) + self.com = self.Port(Passive.empty()) + + self.current = self.ArgParameter(current) + self.voltage = self.ArgParameter(voltage) @abstract_block class TactileSwitch(Switch): - """Abstract class (category) for a tactile switch.""" + """Abstract class (category) for a tactile switch.""" @abstract_block class MechanicalKeyswitch(Switch): - """Abstract class (category) for a mechanical keyboard switch, including sockets.""" + """Abstract class (category) for a mechanical keyboard switch, including sockets.""" @abstract_block class RotaryEncoder(DiscreteComponent): - """Rotary encoder with discrete clicks and a quadrature signal (A/B/Common). - Includes shaft-type encoders as well as thumbwheels.""" - def __init__(self, voltage: RangeLike, current: RangeLike = 0*Amp(tol=0)) -> None: - super().__init__() + """Rotary encoder with discrete clicks and a quadrature signal (A/B/Common). + Includes shaft-type encoders as well as thumbwheels.""" + + def __init__(self, voltage: RangeLike, current: RangeLike = 0 * Amp(tol=0)) -> None: + super().__init__() - self.a = self.Port(Passive.empty()) - self.b = self.Port(Passive.empty()) - self.com = self.Port(Passive.empty()) + self.a = self.Port(Passive.empty()) + self.b = self.Port(Passive.empty()) + self.com = self.Port(Passive.empty()) - self.current = self.ArgParameter(current) - self.voltage = self.ArgParameter(voltage) + self.current = self.ArgParameter(current) + self.voltage = self.ArgParameter(voltage) class RotaryEncoderSwitch(BlockInterfaceMixin[RotaryEncoder]): - """Rotary encoder mixin adding a switch pin (sharing a common with the encoder), - with ratings assumed to be the same between the switch and encoder.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + """Rotary encoder mixin adding a switch pin (sharing a common with the encoder), + with ratings assumed to be the same between the switch and encoder.""" - self.sw = self.Port(Passive.empty(), optional=True) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.sw = self.Port(Passive.empty(), optional=True) @abstract_block class DirectionSwitch(DiscreteComponent): - """Directional switch with a, b, c, d (clockwise) switches and common.""" - def __init__(self, voltage: RangeLike, current: RangeLike = 0*Amp(tol=0)) -> None: - super().__init__() + """Directional switch with a, b, c, d (clockwise) switches and common.""" + + def __init__(self, voltage: RangeLike, current: RangeLike = 0 * Amp(tol=0)) -> None: + super().__init__() - self.a = self.Port(Passive.empty()) - self.b = self.Port(Passive.empty()) - self.c = self.Port(Passive.empty()) - self.d = self.Port(Passive.empty()) - self.com = self.Port(Passive.empty()) + self.a = self.Port(Passive.empty()) + self.b = self.Port(Passive.empty()) + self.c = self.Port(Passive.empty()) + self.d = self.Port(Passive.empty()) + self.com = self.Port(Passive.empty()) - self.current = self.ArgParameter(current) - self.voltage = self.ArgParameter(voltage) + self.current = self.ArgParameter(current) + self.voltage = self.ArgParameter(voltage) class DirectionSwitchCenter(BlockInterfaceMixin[DirectionSwitch]): - """DirectionSwitch mixin adding center switch pin (sharing a common with the encoder), - with ratings assumed to be the same between the switch and encoder.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + """DirectionSwitch mixin adding center switch pin (sharing a common with the encoder), + with ratings assumed to be the same between the switch and encoder.""" - self.center = self.Port(Passive.empty(), optional=True) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.center = self.Port(Passive.empty(), optional=True) class DigitalSwitch(HumanInterface): - """Wrapper around Switch that provides a digital port which is pulled low (to GND) when pressed.""" - def __init__(self) -> None: - super().__init__() + """Wrapper around Switch that provides a digital port which is pulled low (to GND) when pressed.""" + + def __init__(self) -> None: + super().__init__() - self.gnd = self.Port(Ground.empty(), [Common]) - self.out = self.Port(DigitalSource.empty(), [Output]) + self.gnd = self.Port(Ground.empty(), [Common]) + self.out = self.Port(DigitalSource.empty(), [Output]) - @override - def contents(self) -> None: - super().contents() - self.package = self.Block(Switch(current=self.out.link().current_drawn, - voltage=self.out.link().voltage)) + @override + def contents(self) -> None: + super().contents() + self.package = self.Block(Switch(current=self.out.link().current_drawn, voltage=self.out.link().voltage)) - self.connect(self.out, self.package.sw.adapt_to(DigitalSource.low_from_supply(self.gnd))) - self.connect(self.gnd, self.package.com.adapt_to(Ground())) + self.connect(self.out, self.package.sw.adapt_to(DigitalSource.low_from_supply(self.gnd))) + self.connect(self.gnd, self.package.com.adapt_to(Ground())) @abstract_block_default(lambda: DigitalWrapperRotaryEncoder) class DigitalRotaryEncoder(HumanInterface): - """Wrapper around RotaryEncoder that provides digital ports that are pulled low (to GND) when pressed.""" - def __init__(self) -> None: - super().__init__() + """Wrapper around RotaryEncoder that provides digital ports that are pulled low (to GND) when pressed.""" - self.gnd = self.Port(Ground.empty(), [Common]) - self.a = self.Port(DigitalSource.empty()) - self.b = self.Port(DigitalSource.empty()) + def __init__(self) -> None: + super().__init__() + + self.gnd = self.Port(Ground.empty(), [Common]) + self.a = self.Port(DigitalSource.empty()) + self.b = self.Port(DigitalSource.empty()) class DigitalWrapperRotaryEncoder(DigitalRotaryEncoder): - """Basic implementation for DigitalRotaryEncoder as a wrapper around a passive-typed RotaryEncoder.""" - @override - def contents(self) -> None: - super().contents() - self.package = self.Block(RotaryEncoder(current=self.a.link().current_drawn.hull(self.b.link().current_drawn), - voltage=self.a.link().voltage.hull(self.b.link().voltage))) + """Basic implementation for DigitalRotaryEncoder as a wrapper around a passive-typed RotaryEncoder.""" + + @override + def contents(self) -> None: + super().contents() + self.package = self.Block( + RotaryEncoder( + current=self.a.link().current_drawn.hull(self.b.link().current_drawn), + voltage=self.a.link().voltage.hull(self.b.link().voltage), + ) + ) - dio_model = DigitalSource.low_from_supply(self.gnd) - self.connect(self.a, self.package.a.adapt_to(dio_model)) - self.connect(self.b, self.package.b.adapt_to(dio_model)) - self.connect(self.gnd, self.package.com.adapt_to(Ground())) + dio_model = DigitalSource.low_from_supply(self.gnd) + self.connect(self.a, self.package.a.adapt_to(dio_model)) + self.connect(self.b, self.package.b.adapt_to(dio_model)) + self.connect(self.gnd, self.package.com.adapt_to(Ground())) @abstract_block_default(lambda: DigitalWrapperRotaryEncoderWithSwitch) class DigitalRotaryEncoderSwitch(BlockInterfaceMixin[DigitalRotaryEncoder]): - """DigitalRotaryEncoder mixin adding a switch pin.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + """DigitalRotaryEncoder mixin adding a switch pin.""" - self.sw = self.Port(DigitalSource.empty(), optional=True) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.sw = self.Port(DigitalSource.empty(), optional=True) class DigitalWrapperRotaryEncoderWithSwitch(DigitalRotaryEncoderSwitch, DigitalWrapperRotaryEncoder, GeneratorBlock): - @override - def contents(self) -> None: - super().contents() - self.generator_param(self.sw.is_connected()) + @override + def contents(self) -> None: + super().contents() + self.generator_param(self.sw.is_connected()) - @override - def generate(self) -> None: - super().generate() - if self.get(self.sw.is_connected()): - package_sw = self.package.with_mixin(RotaryEncoderSwitch()) - dio_model = DigitalSource.low_from_supply(self.gnd) - self.connect(self.sw, package_sw.sw.adapt_to(dio_model)) + @override + def generate(self) -> None: + super().generate() + if self.get(self.sw.is_connected()): + package_sw = self.package.with_mixin(RotaryEncoderSwitch()) + dio_model = DigitalSource.low_from_supply(self.gnd) + self.connect(self.sw, package_sw.sw.adapt_to(dio_model)) @abstract_block_default(lambda: DigitalWrapperDirectionSwitch) class DigitalDirectionSwitch(HumanInterface): - """Wrapper around DirectionSwitch that provides digital ports that are pulled low (to GND) when pressed.""" - def __init__(self) -> None: - super().__init__() + """Wrapper around DirectionSwitch that provides digital ports that are pulled low (to GND) when pressed.""" - self.gnd = self.Port(Ground.empty(), [Common]) - self.a = self.Port(DigitalSource.empty()) - self.b = self.Port(DigitalSource.empty()) - self.c = self.Port(DigitalSource.empty()) - self.d = self.Port(DigitalSource.empty()) + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground.empty(), [Common]) + self.a = self.Port(DigitalSource.empty()) + self.b = self.Port(DigitalSource.empty()) + self.c = self.Port(DigitalSource.empty()) + self.d = self.Port(DigitalSource.empty()) -class DigitalWrapperDirectionSwitch(DigitalDirectionSwitch): - """Basic implementation for DigitalDirectionSwitch as a wrapper around a passive-typed DirectionSwitch.""" - @override - def contents(self) -> None: - super().contents() - self.package = self.Block(DirectionSwitch(current=self.a.link().current_drawn.hull(self.b.link().current_drawn), - voltage=self.a.link().voltage.hull(self.b.link().voltage))) - dio_model = DigitalSource.low_from_supply(self.gnd) - self.connect(self.a, self.package.a.adapt_to(dio_model)) - self.connect(self.b, self.package.b.adapt_to(dio_model)) - self.connect(self.c, self.package.c.adapt_to(dio_model)) - self.connect(self.d, self.package.d.adapt_to(dio_model)) - self.connect(self.gnd, self.package.com.adapt_to(Ground())) +class DigitalWrapperDirectionSwitch(DigitalDirectionSwitch): + """Basic implementation for DigitalDirectionSwitch as a wrapper around a passive-typed DirectionSwitch.""" + + @override + def contents(self) -> None: + super().contents() + self.package = self.Block( + DirectionSwitch( + current=self.a.link().current_drawn.hull(self.b.link().current_drawn), + voltage=self.a.link().voltage.hull(self.b.link().voltage), + ) + ) + + dio_model = DigitalSource.low_from_supply(self.gnd) + self.connect(self.a, self.package.a.adapt_to(dio_model)) + self.connect(self.b, self.package.b.adapt_to(dio_model)) + self.connect(self.c, self.package.c.adapt_to(dio_model)) + self.connect(self.d, self.package.d.adapt_to(dio_model)) + self.connect(self.gnd, self.package.com.adapt_to(Ground())) @abstract_block_default(lambda: DigitalWrapperDirectionSwitchWithCenter) class DigitalDirectionSwitchCenter(BlockInterfaceMixin[DigitalDirectionSwitch]): - """DigitalRotaryEncoder mixin adding a switch pin.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - self.center = self.Port(DigitalSource.empty(), optional=True) - - -class DigitalWrapperDirectionSwitchWithCenter(DigitalDirectionSwitchCenter, DigitalWrapperDirectionSwitch, - GeneratorBlock): - @override - def contents(self) -> None: - super().contents() - self.generator_param(self.center.is_connected()) - - @override - def generate(self) -> None: - super().generate() - if self.get(self.center.is_connected()): - package_sw = self.package.with_mixin(DirectionSwitchCenter()) - dio_model = DigitalSource.low_from_supply(self.gnd) - self.connect(self.center, package_sw.center.adapt_to(dio_model)) + """DigitalRotaryEncoder mixin adding a switch pin.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.center = self.Port(DigitalSource.empty(), optional=True) + + +class DigitalWrapperDirectionSwitchWithCenter( + DigitalDirectionSwitchCenter, DigitalWrapperDirectionSwitch, GeneratorBlock +): + @override + def contents(self) -> None: + super().contents() + self.generator_param(self.center.is_connected()) + + @override + def generate(self) -> None: + super().generate() + if self.get(self.center.is_connected()): + package_sw = self.package.with_mixin(DirectionSwitchCenter()) + dio_model = DigitalSource.low_from_supply(self.gnd) + self.connect(self.center, package_sw.center.adapt_to(dio_model)) diff --git a/edg/abstract_parts/AbstractTestPoint.py b/edg/abstract_parts/AbstractTestPoint.py index 9fa38c335..7776443fd 100644 --- a/edg/abstract_parts/AbstractTestPoint.py +++ b/edg/abstract_parts/AbstractTestPoint.py @@ -12,209 +12,221 @@ @abstract_block class TestPoint(InternalSubcircuit, Block): - """Abstract test point that can take a name as a string, used as the footprint value. - """ - def __init__(self, tp_name: StringLike = "") -> None: - super().__init__() - self.io = self.Port(Passive(), [InOut]) - self.tp_name = self.ArgParameter(tp_name) + """Abstract test point that can take a name as a string, used as the footprint value.""" + + def __init__(self, tp_name: StringLike = "") -> None: + super().__init__() + self.io = self.Port(Passive(), [InOut]) + self.tp_name = self.ArgParameter(tp_name) @non_library class BaseTypedTestPoint(TypedTestPoint, Block): - """Base class with utility infrastructure for typed test points""" - def __init__(self, tp_name: StringLike = "") -> None: - super().__init__() - self.io: Port - self.tp_name = self.ArgParameter(tp_name) - self.tp = self.Block(TestPoint(tp_name=StringExpr())) + """Base class with utility infrastructure for typed test points""" + + def __init__(self, tp_name: StringLike = "") -> None: + super().__init__() + self.io: Port + self.tp_name = self.ArgParameter(tp_name) + self.tp = self.Block(TestPoint(tp_name=StringExpr())) - @override - def contents(self) -> None: - super().contents() - self.assign(self.tp.tp_name, (self.tp_name == "").then_else(self.io.link().name(), self.tp_name)) + @override + def contents(self) -> None: + super().contents() + self.assign(self.tp.tp_name, (self.tp_name == "").then_else(self.io.link().name(), self.tp_name)) @non_library class BaseRfTestPoint(TypedTestPoint, Block): - """Base class with utility infrastructure for typed RF test points.""" - def __init__(self, tp_name: StringLike = "") -> None: - super().__init__() - self.tp_name = self.ArgParameter(tp_name) - self.conn = self.Block(RfConnector()) - self.gnd = self.Export(self.conn.gnd, [Common]) - self.io: Port + """Base class with utility infrastructure for typed RF test points.""" + + def __init__(self, tp_name: StringLike = "") -> None: + super().__init__() + self.tp_name = self.ArgParameter(tp_name) + self.conn = self.Block(RfConnector()) + self.gnd = self.Export(self.conn.gnd, [Common]) + self.io: Port - @override - def contents(self) -> None: - super().contents() - conn_tp = self.conn.with_mixin(RfConnectorTestPoint(StringExpr())) - self.assign(conn_tp.tp_name, (self.tp_name == "").then_else(self.io.link().name(), self.tp_name)) + @override + def contents(self) -> None: + super().contents() + conn_tp = self.conn.with_mixin(RfConnectorTestPoint(StringExpr())) + self.assign(conn_tp.tp_name, (self.tp_name == "").then_else(self.io.link().name(), self.tp_name)) class GroundTestPoint(BaseTypedTestPoint, Block): - """Test point with a VoltageSink port.""" - def __init__(self, *args: Any) -> None: - super().__init__(*args) - self.io = self.Port(Ground.empty(), [InOut]) - self.connect(self.io, self.tp.io.adapt_to(Ground())) + """Test point with a VoltageSink port.""" - def connected(self, io: Port[GroundLink]) -> 'GroundTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + def __init__(self, *args: Any) -> None: + super().__init__(*args) + self.io = self.Port(Ground.empty(), [InOut]) + self.connect(self.io, self.tp.io.adapt_to(Ground())) + + def connected(self, io: Port[GroundLink]) -> "GroundTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class VoltageTestPoint(BaseTypedTestPoint, Block): - """Test point with a VoltageSink port.""" - def __init__(self, *args: Any) -> None: - super().__init__(*args) - self.io = self.Port(VoltageSink.empty(), [InOut]) - self.connect(self.io, self.tp.io.adapt_to(VoltageSink())) + """Test point with a VoltageSink port.""" + + def __init__(self, *args: Any) -> None: + super().__init__(*args) + self.io = self.Port(VoltageSink.empty(), [InOut]) + self.connect(self.io, self.tp.io.adapt_to(VoltageSink())) - def connected(self, io: Port[VoltageLink]) -> 'VoltageTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + def connected(self, io: Port[VoltageLink]) -> "VoltageTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class DigitalTestPoint(BaseTypedTestPoint, Block): - """Test point with a DigitalSink port.""" - def __init__(self, *args: Any) -> None: - super().__init__(*args) - self.io = self.Port(DigitalSink.empty(), [InOut]) - self.connect(self.io, self.tp.io.adapt_to(DigitalSink())) + """Test point with a DigitalSink port.""" - def connected(self, io: Port[DigitalLink]) -> 'DigitalTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + def __init__(self, *args: Any) -> None: + super().__init__(*args) + self.io = self.Port(DigitalSink.empty(), [InOut]) + self.connect(self.io, self.tp.io.adapt_to(DigitalSink())) + + def connected(self, io: Port[DigitalLink]) -> "DigitalTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class DigitalArrayTestPoint(TypedTestPoint, GeneratorBlock): - """Creates an array of Digital test points, sized from the port array's connections.""" - def __init__(self, tp_name: StringLike = '') -> None: - super().__init__() - self.io = self.Port(Vector(DigitalSink.empty()), [InOut]) - self.tp_name = self.ArgParameter(tp_name) - self.generator_param(self.io.requested(), self.tp_name) - - @override - def generate(self) -> None: - super().generate() - self.tp = ElementDict[DigitalTestPoint]() - for requested in self.get(self.io.requested()): - # TODO: link() on Vector is not supported, so we leave the naming to the leaf link in the leaf test point - if self.get(self.tp_name) == '': - tp = self.tp[requested] = self.Block(DigitalTestPoint()) - else: - tp = self.tp[requested] = self.Block(DigitalTestPoint(self.tp_name + f'.{requested}')) - self.connect(self.io.append_elt(DigitalSink.empty(), requested), tp.io) + """Creates an array of Digital test points, sized from the port array's connections.""" + + def __init__(self, tp_name: StringLike = "") -> None: + super().__init__() + self.io = self.Port(Vector(DigitalSink.empty()), [InOut]) + self.tp_name = self.ArgParameter(tp_name) + self.generator_param(self.io.requested(), self.tp_name) + + @override + def generate(self) -> None: + super().generate() + self.tp = ElementDict[DigitalTestPoint]() + for requested in self.get(self.io.requested()): + # TODO: link() on Vector is not supported, so we leave the naming to the leaf link in the leaf test point + if self.get(self.tp_name) == "": + tp = self.tp[requested] = self.Block(DigitalTestPoint()) + else: + tp = self.tp[requested] = self.Block(DigitalTestPoint(self.tp_name + f".{requested}")) + self.connect(self.io.append_elt(DigitalSink.empty(), requested), tp.io) class AnalogTestPoint(BaseTypedTestPoint, Block): - """Test point with a AnalogSink port""" - def __init__(self, *args: Any) -> None: - super().__init__(*args) - self.io = self.Port(AnalogSink.empty(), [InOut]) - self.connect(self.io, self.tp.io.adapt_to(AnalogSink())) + """Test point with a AnalogSink port""" + + def __init__(self, *args: Any) -> None: + super().__init__(*args) + self.io = self.Port(AnalogSink.empty(), [InOut]) + self.connect(self.io, self.tp.io.adapt_to(AnalogSink())) - def connected(self, io: Port[AnalogLink]) -> 'AnalogTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + def connected(self, io: Port[AnalogLink]) -> "AnalogTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class AnalogCoaxTestPoint(BaseRfTestPoint, Block): - """Test point with a AnalogSink port and using a coax connector with shielding connected to gnd. - No impedance matching, this is intended for lower frequency signals where the wavelength would be - much longer than the test lead length""" - def __init__(self, *args: Any) -> None: - super().__init__(*args) - self.io = self.Export(self.conn.sig.adapt_to(AnalogSink()), [InOut]) + """Test point with a AnalogSink port and using a coax connector with shielding connected to gnd. + No impedance matching, this is intended for lower frequency signals where the wavelength would be + much longer than the test lead length""" - def connected(self, io: Port[AnalogLink]) -> 'AnalogCoaxTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + def __init__(self, *args: Any) -> None: + super().__init__(*args) + self.io = self.Export(self.conn.sig.adapt_to(AnalogSink()), [InOut]) + + def connected(self, io: Port[AnalogLink]) -> "AnalogCoaxTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class I2cTestPoint(TypedTestPoint, Block): - """Two test points for I2C SDA and SCL""" - def __init__(self, tp_name: StringLike = "") -> None: - super().__init__() - self.io = self.Port(I2cTarget(DigitalBidir.empty()), [InOut]) - self.tp_name = self.ArgParameter(tp_name) - - @override - def contents(self) -> None: - super().contents() - name_prefix = (self.tp_name == '').then_else(self.io.link().name(), self.tp_name) - self.tp_scl = self.Block(DigitalTestPoint(name_prefix + '.scl')) - self.tp_sda = self.Block(DigitalTestPoint(name_prefix + '.sda')) - self.connect(self.tp_scl.io, self.io.scl) - self.connect(self.tp_sda.io, self.io.sda) - - def connected(self, io: Port[I2cLink]) -> 'I2cTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + """Two test points for I2C SDA and SCL""" + + def __init__(self, tp_name: StringLike = "") -> None: + super().__init__() + self.io = self.Port(I2cTarget(DigitalBidir.empty()), [InOut]) + self.tp_name = self.ArgParameter(tp_name) + + @override + def contents(self) -> None: + super().contents() + name_prefix = (self.tp_name == "").then_else(self.io.link().name(), self.tp_name) + self.tp_scl = self.Block(DigitalTestPoint(name_prefix + ".scl")) + self.tp_sda = self.Block(DigitalTestPoint(name_prefix + ".sda")) + self.connect(self.tp_scl.io, self.io.scl) + self.connect(self.tp_sda.io, self.io.sda) + + def connected(self, io: Port[I2cLink]) -> "I2cTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class SpiTestPoint(TypedTestPoint, Block): - """Test points for SPI""" - def __init__(self, tp_name: StringLike = "") -> None: - super().__init__() - self.io = self.Port(SpiPeripheral(DigitalBidir.empty()), [InOut]) - self.tp_name = self.ArgParameter(tp_name) - - @override - def contents(self) -> None: - super().contents() - name_prefix = (self.tp_name == '').then_else(self.io.link().name(), self.tp_name) - self.tp_sck = self.Block(DigitalTestPoint(name_prefix + '.sck')) - self.tp_mosi = self.Block(DigitalTestPoint(name_prefix + '.mosi')) - self.tp_miso = self.Block(DigitalTestPoint(name_prefix + '.miso')) - self.connect(self.tp_sck.io, self.io.sck) - self.connect(self.tp_mosi.io, self.io.mosi) - self.connect(self.tp_miso.io, self.io.miso) - - def connected(self, io: Port[SpiLink]) -> 'SpiTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + """Test points for SPI""" + + def __init__(self, tp_name: StringLike = "") -> None: + super().__init__() + self.io = self.Port(SpiPeripheral(DigitalBidir.empty()), [InOut]) + self.tp_name = self.ArgParameter(tp_name) + + @override + def contents(self) -> None: + super().contents() + name_prefix = (self.tp_name == "").then_else(self.io.link().name(), self.tp_name) + self.tp_sck = self.Block(DigitalTestPoint(name_prefix + ".sck")) + self.tp_mosi = self.Block(DigitalTestPoint(name_prefix + ".mosi")) + self.tp_miso = self.Block(DigitalTestPoint(name_prefix + ".miso")) + self.connect(self.tp_sck.io, self.io.sck) + self.connect(self.tp_mosi.io, self.io.mosi) + self.connect(self.tp_miso.io, self.io.miso) + + def connected(self, io: Port[SpiLink]) -> "SpiTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class CanControllerTestPoint(TypedTestPoint, Block): - """Two test points for CAN controller-side TXD and RXD""" - def __init__(self, tp_name: StringLike = "") -> None: - super().__init__() - self.io = self.Port(CanPassivePort(DigitalBidir.empty()), [InOut]) - self.tp_name = self.ArgParameter(tp_name) - - @override - def contents(self) -> None: - super().contents() - name_prefix = (self.tp_name == '').then_else(self.io.link().name(), self.tp_name) - self.tp_txd = self.Block(DigitalTestPoint(name_prefix + '.txd')) - self.tp_rxd = self.Block(DigitalTestPoint(name_prefix + '.rxd')) - self.connect(self.tp_txd.io, self.io.txd) - self.connect(self.tp_rxd.io, self.io.rxd) - - def connected(self, io: Port[CanLogicLink]) -> 'CanControllerTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + """Two test points for CAN controller-side TXD and RXD""" + + def __init__(self, tp_name: StringLike = "") -> None: + super().__init__() + self.io = self.Port(CanPassivePort(DigitalBidir.empty()), [InOut]) + self.tp_name = self.ArgParameter(tp_name) + + @override + def contents(self) -> None: + super().contents() + name_prefix = (self.tp_name == "").then_else(self.io.link().name(), self.tp_name) + self.tp_txd = self.Block(DigitalTestPoint(name_prefix + ".txd")) + self.tp_rxd = self.Block(DigitalTestPoint(name_prefix + ".rxd")) + self.connect(self.tp_txd.io, self.io.txd) + self.connect(self.tp_rxd.io, self.io.rxd) + + def connected(self, io: Port[CanLogicLink]) -> "CanControllerTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class CanDiffTestPoint(TypedTestPoint, Block): - """Two test points for CAN differential-side canh and canl""" - def __init__(self, tp_name: StringLike = "") -> None: - super().__init__() - self.io = self.Port(CanDiffPort(DigitalBidir.empty()), [InOut]) - self.tp_name = self.ArgParameter(tp_name) - - @override - def contents(self) -> None: - super().contents() - name_prefix = (self.tp_name == '').then_else(self.io.link().name(), self.tp_name) - self.tp_canh = self.Block(DigitalTestPoint(name_prefix + '.canh')) - self.tp_canl = self.Block(DigitalTestPoint(name_prefix + '.canl')) - self.connect(self.tp_canh.io, self.io.canh) - self.connect(self.tp_canl.io, self.io.canl) - - def connected(self, io: Port[CanLogicLink]) -> 'CanDiffTestPoint': - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + """Two test points for CAN differential-side canh and canl""" + + def __init__(self, tp_name: StringLike = "") -> None: + super().__init__() + self.io = self.Port(CanDiffPort(DigitalBidir.empty()), [InOut]) + self.tp_name = self.ArgParameter(tp_name) + + @override + def contents(self) -> None: + super().contents() + name_prefix = (self.tp_name == "").then_else(self.io.link().name(), self.tp_name) + self.tp_canh = self.Block(DigitalTestPoint(name_prefix + ".canh")) + self.tp_canl = self.Block(DigitalTestPoint(name_prefix + ".canl")) + self.connect(self.tp_canh.io, self.io.canh) + self.connect(self.tp_canl.io, self.io.canl) + + def connected(self, io: Port[CanLogicLink]) -> "CanDiffTestPoint": + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self diff --git a/edg/abstract_parts/AbstractTvsDiode.py b/edg/abstract_parts/AbstractTvsDiode.py index aa4b455bd..74a86060f 100644 --- a/edg/abstract_parts/AbstractTvsDiode.py +++ b/edg/abstract_parts/AbstractTvsDiode.py @@ -19,8 +19,8 @@ class TvsDiode(BaseDiode): TODO: model capacitance frequency? model breakdown and clamping voltage? TODO: how does this differ from Zener diodes? """ - def __init__(self, working_voltage: RangeLike, *, - capacitance: RangeLike = Range.all()) -> None: + + def __init__(self, working_voltage: RangeLike, *, capacitance: RangeLike = Range.all()) -> None: super().__init__() self.working_voltage = self.ArgParameter(working_voltage) @@ -33,6 +33,7 @@ def __init__(self, working_voltage: RangeLike, *, class ProtectionTvsDiode(Protection): """TVS diode across a power rail""" + def __init__(self, working_voltage: RangeLike): super().__init__() @@ -45,14 +46,20 @@ def __init__(self, working_voltage: RangeLike): def contents(self) -> None: super().contents() self.diode = self.Block(TvsDiode(working_voltage=self.working_voltage)) - self.connect(self.diode.cathode.adapt_to(VoltageSink( - voltage_limits=self.diode.actual_breakdown_voltage, - )), self.pwr) + self.connect( + self.diode.cathode.adapt_to( + VoltageSink( + voltage_limits=self.diode.actual_breakdown_voltage, + ) + ), + self.pwr, + ) self.connect(self.diode.anode.adapt_to(Ground()), self.gnd) class DigitalTvsDiode(Protection): """TVS diode protecting a signal line""" + def __init__(self, working_voltage: RangeLike, *, capacitance: RangeLike = Range.all()): super().__init__() @@ -66,7 +73,12 @@ def __init__(self, working_voltage: RangeLike, *, capacitance: RangeLike = Range def contents(self) -> None: super().contents() self.diode = self.Block(TvsDiode(working_voltage=self.working_voltage, capacitance=self.capacitance)) - self.connect(self.diode.cathode.adapt_to(DigitalSink( - voltage_limits=self.diode.actual_breakdown_voltage, - )), self.io) + self.connect( + self.diode.cathode.adapt_to( + DigitalSink( + voltage_limits=self.diode.actual_breakdown_voltage, + ) + ), + self.io, + ) self.connect(self.diode.anode.adapt_to(Ground()), self.gnd) diff --git a/edg/abstract_parts/CanTransceiver.py b/edg/abstract_parts/CanTransceiver.py index 1f7443572..40dadccc6 100644 --- a/edg/abstract_parts/CanTransceiver.py +++ b/edg/abstract_parts/CanTransceiver.py @@ -4,29 +4,30 @@ @abstract_block class CanTransceiver(Interface, Block): - """Abstract CAN transceiver""" - def __init__(self) -> None: - super().__init__() + """Abstract CAN transceiver""" - self.pwr = self.Port(VoltageSink.empty(), [Power]) # for isolated converters, this is the logic side supply - self.gnd = self.Port(Ground.empty(), [Common]) + def __init__(self) -> None: + super().__init__() - self.controller = self.Port(CanTransceiverPort.empty(), [Input]) - self.can = self.Port(CanDiffPort.empty(), [Output]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) # for isolated converters, this is the logic side supply + self.gnd = self.Port(Ground.empty(), [Common]) + + self.controller = self.Port(CanTransceiverPort.empty(), [Input]) + self.can = self.Port(CanDiffPort.empty(), [Output]) @abstract_block class IsolatedCanTransceiver(CanTransceiver): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.can_pwr = self.Port(VoltageSink.empty()) # no implicit connect tags for isolated side - self.can_gnd = self.Port(Ground.empty()) + self.can_pwr = self.Port(VoltageSink.empty()) # no implicit connect tags for isolated side + self.can_gnd = self.Port(Ground.empty()) @abstract_block class CanEsdDiode(Protection, Block): - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground.empty(), [Common]) - self.can = self.Port(CanDiffPort.empty(), [InOut]) + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground.empty(), [Common]) + self.can = self.Port(CanDiffPort.empty(), [InOut]) diff --git a/edg/abstract_parts/Categories.py b/edg/abstract_parts/Categories.py index f027c6939..7174f76d4 100644 --- a/edg/abstract_parts/Categories.py +++ b/edg/abstract_parts/Categories.py @@ -8,374 +8,422 @@ @abstract_block class DiscreteApplication(Block): - """Subcircuit around a single discrete (and usually passive) component.""" - pass + """Subcircuit around a single discrete (and usually passive) component.""" + + pass @abstract_block class Analog(Block): - """Analog blocks that don't fit into one of the other categories""" - pass + """Analog blocks that don't fit into one of the other categories""" + + pass @abstract_block class OpampApplication(Analog): - """Opamp-based circuits, typically one that perform some function on signals""" - pass + """Opamp-based circuits, typically one that perform some function on signals""" + + pass @abstract_block class Filter(Block): - """Signal conditioning subcircuit.""" - pass + """Signal conditioning subcircuit.""" + + pass @abstract_block class AnalogFilter(Filter): - """Analog signal conditioning subcircuit.""" - pass + """Analog signal conditioning subcircuit.""" + + pass @abstract_block class RfFilter(AnalogFilter): - """RF signal conditioning subcircuit.""" - pass + """RF signal conditioning subcircuit.""" + + pass @abstract_block class DigitalFilter(Filter): - """Digital signal conditioning block.""" - pass + """Digital signal conditioning block.""" + + pass @abstract_block class ProgrammableController(Block): - """General programmable controller.""" - pass + """General programmable controller.""" + + pass @abstract_block class Microcontroller(ProgrammableController): - """Microcontroller (with embedded-class processor) with its surrounding application circuit.""" - pass + """Microcontroller (with embedded-class processor) with its surrounding application circuit.""" + + pass @abstract_block class Fpga(ProgrammableController): - """FPGA with its surrounding application circuit.""" - pass + """FPGA with its surrounding application circuit.""" + + pass @abstract_block class Memory(Block): - """Memory device (including sockets and card sockets) with its surrounding application circuit.""" - pass + """Memory device (including sockets and card sockets) with its surrounding application circuit.""" + + pass @abstract_block class RealtimeClock(Block): - """Realtime clock device.""" - pass + """Realtime clock device.""" + + pass @abstract_block class Interface(Block): - """Interface devices, eg CAN transceiver (CAN <-> SPI / I2C interface), - and including analog interfaces (ADCs, DACs).""" - pass + """Interface devices, eg CAN transceiver (CAN <-> SPI / I2C interface), + and including analog interfaces (ADCs, DACs).""" + + pass @abstract_block class AnalogToDigital(Interface): - pass + pass @abstract_block class DigitalToAnalog(Interface): - pass + pass @abstract_block class SpeakerDriver(Interface): - pass + pass @abstract_block class IoExpander(Interface): - pass + pass @abstract_block class BitBangAdapter(Interface): - """Adapters that break out a structured Bundle to component wires, useful when bit-banging those protocols""" - pass + """Adapters that break out a structured Bundle to component wires, useful when bit-banging those protocols""" + + pass @abstract_block class Radiofrequency(Block): - """Radiofrequency devices.""" - pass + """Radiofrequency devices.""" + + pass @abstract_block class PowerConditioner(Block): - """Power conditioning circuits that provide a stable and/or safe power supply, eg voltage regulators""" - pass + """Power conditioning circuits that provide a stable and/or safe power supply, eg voltage regulators""" + + pass @abstract_block class PowerSwitch(Block): - """Power switching circuits, eg FET switches and motor drivers""" - pass + """Power switching circuits, eg FET switches and motor drivers""" + + pass @abstract_block class MotorDriver(PowerSwitch): - pass + pass @abstract_block class BrushedMotorDriver(MotorDriver): - """A brushed motor driver, or at least the power stage for one.""" - pass + """A brushed motor driver, or at least the power stage for one.""" + + pass @abstract_block class BldcDriver(MotorDriver): - """A brushless motor driver, or at least the power stage for one - may be as simple a 3 half-bridges.""" - pass + """A brushless motor driver, or at least the power stage for one - may be as simple a 3 half-bridges.""" + + pass @abstract_block class Connector(Block): - """Connectors, including card sockets.""" - pass + """Connectors, including card sockets.""" + + pass @abstract_block class PowerSource(Block): - """Power sources, including connectors that also supply power.""" - pass + """Power sources, including connectors that also supply power.""" + + pass @abstract_block class HumanInterface(Block): - """Devices for human interface, eg switches, displays, LEDs""" - pass + """Devices for human interface, eg switches, displays, LEDs""" + + pass @abstract_block class Display(HumanInterface): - """Pixel displays.""" - pass + """Pixel displays.""" + + pass @abstract_block class Lcd(Display): - """LCD display, where pixels absorb / reflect light, but do not directly emit light (eg, use a backlight, or are transflective).""" - pass + """LCD display, where pixels absorb / reflect light, but do not directly emit light (eg, use a backlight, or are transflective).""" + + pass @abstract_block class Oled(Display): - """OLED display, with the pixel density of an LCD but with infinite contrast and no backlight.""" - pass + """OLED display, with the pixel density of an LCD but with infinite contrast and no backlight.""" + + pass @abstract_block class EInk(Display): - """E-ink display, which retains the image after power is removed.""" - pass + """E-ink display, which retains the image after power is removed.""" + + pass @abstract_block class Light(HumanInterface): - """Discrete lights.""" - pass + """Discrete lights.""" + + pass @abstract_block class Sensor(Block): - """Any kind of sensor with any interface. Multi-packed sensors may inherit from multiple categories""" - pass + """Any kind of sensor with any interface. Multi-packed sensors may inherit from multiple categories""" + + pass @abstract_block class CurrentSensor(Sensor): - pass + pass @abstract_block class Accelerometer(Sensor): - pass + pass @abstract_block class Gyroscope(Sensor): - pass + pass @abstract_block class EnvironmentalSensor(Sensor): - pass + pass @abstract_block class TemperatureSensor(EnvironmentalSensor): - pass + pass @abstract_block class HumiditySensor(EnvironmentalSensor): - pass + pass @abstract_block class PressureSensor(EnvironmentalSensor): - """Sensors measuring ambient pressure""" - pass + """Sensors measuring ambient pressure""" + + pass @abstract_block class GasSensor(EnvironmentalSensor): - """Sensors measuring gas concentration, including non-particle IAQ, TVOC, eCO2, and CO2 sensors.""" - pass + """Sensors measuring gas concentration, including non-particle IAQ, TVOC, eCO2, and CO2 sensors.""" + + pass @abstract_block class LightSensor(Sensor): - pass + pass @abstract_block class MagneticSensor(Sensor): - pass + pass @abstract_block class MagneticSwitch(MagneticSensor): - """A switch that is activated by a magnetic field, including omnipolar and bipolar devices.""" - pass + """A switch that is activated by a magnetic field, including omnipolar and bipolar devices.""" + + pass @abstract_block class Magnetometer(MagneticSensor): - """Linear response magnetic field sensor, potentially with multiple axes""" - pass + """Linear response magnetic field sensor, potentially with multiple axes""" + + pass @abstract_block class Microphone(Sensor): - pass + pass @abstract_block class Camera(Sensor): - """Imaging sensors, including visible / RGB, IR, and thermal.""" - pass + """Imaging sensors, including visible / RGB, IR, and thermal.""" + + pass @abstract_block class DistanceSensor(Sensor): - pass + pass @abstract_block class Protection(Block): - """Circuit protection elements, eg TVS diodes, fuses""" - pass + """Circuit protection elements, eg TVS diodes, fuses""" + + pass @abstract_block class Testing(Block): - """Blocks for testing (eg, test points) and programming (eg, programming headers).""" - pass + """Blocks for testing (eg, test points) and programming (eg, programming headers).""" + + pass @abstract_block class MultipackDevice(Block): - """A multipack device (e.g., dualpack opamp, quadpack resistor array) which blocks across the design - can be merged into.""" - pass + """A multipack device (e.g., dualpack opamp, quadpack resistor array) which blocks across the design + can be merged into.""" + + pass @abstract_block class ProgrammingConnector(Connector, Testing): - """Programming / debug / JTAG connectors.""" - pass + """Programming / debug / JTAG connectors.""" + + pass @abstract_block class TypedTestPoint(Testing): - """Test point with a typed port (eg, VoltageSink, instead of Passive).""" - pass + """Test point with a typed port (eg, VoltageSink, instead of Passive).""" + + pass @abstract_block class TypedJumper(Testing): - """Jumper with typed ports (eg, VoltageSource-VoltageSink, instead of Passive).""" - pass + """Jumper with typed ports (eg, VoltageSource-VoltageSink, instead of Passive).""" + + pass @abstract_block class InternalSubcircuit(InternalBlock): - """Internal blocks that are primarily an implementation detail or not re-usable""" - pass + """Internal blocks that are primarily an implementation detail or not re-usable""" + + pass @abstract_block class DiscreteComponent(InternalBlock): - """Discrete component that typically provides untyped ports (not to be be used directly), as a component to be used in an application circuit.""" - pass + """Discrete component that typically provides untyped ports (not to be be used directly), as a component to be used in an application circuit.""" + + pass @abstract_block class DiscreteSemiconductor(DiscreteComponent): - """Discrete semiconductor product, eg diodes and FETs, typically used as part of an application circuit.""" - pass + """Discrete semiconductor product, eg diodes and FETs, typically used as part of an application circuit.""" + + pass @abstract_block class PassiveComponent(DiscreteComponent): - """Passives components, typically used as part of an application circuit.""" - pass + """Passives components, typically used as part of an application circuit.""" + + pass @abstract_block class DummyDevice(InternalBlock): - """Non-physical "device" used to affect parameters.""" - pass + """Non-physical "device" used to affect parameters.""" + + pass @abstract_block class IdealModel(InternalBlock): - """Ideal model device that can be used as a placeholder to get a design compiling - but has no physical implementation.""" - def __init__(self, *args: Any, allow_ideal: BoolLike = False, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.allow_ideal = self.ArgParameter(allow_ideal) + """Ideal model device that can be used as a placeholder to get a design compiling + but has no physical implementation.""" + + def __init__(self, *args: Any, allow_ideal: BoolLike = False, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.allow_ideal = self.ArgParameter(allow_ideal) - @override - def contents(self) -> None: - super().contents() - self.require(self.allow_ideal, "ideal model") + @override + def contents(self) -> None: + super().contents() + self.require(self.allow_ideal, "ideal model") @abstract_block class DeprecatedBlock(InternalBlock): - """Base class for blocks that are deprecated and planned to be removed""" - pass + """Base class for blocks that are deprecated and planned to be removed""" + + pass @abstract_block class Label(DeprecatedBlock): - """DEPRECATED: non-circuit footprints should be added in layout as non-schematic items. - Nonfunctional footprint, including copper and silkscreen labels.""" - pass + """DEPRECATED: non-circuit footprints should be added in layout as non-schematic items. + Nonfunctional footprint, including copper and silkscreen labels.""" + + pass @abstract_block class Mechanical(DeprecatedBlock): - """DEPRECATED: non-circuit footprints should be added in layout as non-schematic items. - Nonelectrical footprint, including plated and NPTH mounting holes.""" - pass + """DEPRECATED: non-circuit footprints should be added in layout as non-schematic items. + Nonelectrical footprint, including plated and NPTH mounting holes.""" + + pass diff --git a/edg/abstract_parts/CustomDiode.py b/edg/abstract_parts/CustomDiode.py index 3b5e4cda6..79168e1d7 100644 --- a/edg/abstract_parts/CustomDiode.py +++ b/edg/abstract_parts/CustomDiode.py @@ -7,27 +7,35 @@ class CustomDiode(Diode, FootprintBlock, GeneratorBlock): - def __init__(self, *args: Any, footprint_spec: StringLike = "", - manufacturer_spec: StringLike = "", part_spec: StringLike = "", **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.footprint_spec = self.ArgParameter(footprint_spec) # actual_footprint left to the actual footprint - self.manufacturer_spec = self.ArgParameter(manufacturer_spec) - self.part_spec = self.ArgParameter(part_spec) + def __init__( + self, + *args: Any, + footprint_spec: StringLike = "", + manufacturer_spec: StringLike = "", + part_spec: StringLike = "", + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self.footprint_spec = self.ArgParameter(footprint_spec) # actual_footprint left to the actual footprint + self.manufacturer_spec = self.ArgParameter(manufacturer_spec) + self.part_spec = self.ArgParameter(part_spec) - self.generator_param(self.footprint_spec) + self.generator_param(self.footprint_spec) - # use ideal specs, which can be overridden with refinements - self.assign(self.actual_voltage_rating, Range.all()) - self.assign(self.actual_current_rating, Range.all()) - self.assign(self.actual_voltage_drop, Range.zero_to_upper(0)) - self.assign(self.actual_reverse_recovery_time, Range.zero_to_upper(0)) + # use ideal specs, which can be overridden with refinements + self.assign(self.actual_voltage_rating, Range.all()) + self.assign(self.actual_current_rating, Range.all()) + self.assign(self.actual_voltage_drop, Range.zero_to_upper(0)) + self.assign(self.actual_reverse_recovery_time, Range.zero_to_upper(0)) - @override - def generate(self) -> None: - self.footprint( - self._standard_footprint().REFDES_PREFIX, self.footprint_spec, - self._standard_footprint()._make_pinning(self, self.get(self.footprint_spec)), - mfr=self.manufacturer_spec, part=self.part_spec, - value=self.part_spec, - datasheet="" - ) + @override + def generate(self) -> None: + self.footprint( + self._standard_footprint().REFDES_PREFIX, + self.footprint_spec, + self._standard_footprint()._make_pinning(self, self.get(self.footprint_spec)), + mfr=self.manufacturer_spec, + part=self.part_spec, + value=self.part_spec, + datasheet="", + ) diff --git a/edg/abstract_parts/CustomFet.py b/edg/abstract_parts/CustomFet.py index 1bfdcdf64..d2ca6e9e5 100644 --- a/edg/abstract_parts/CustomFet.py +++ b/edg/abstract_parts/CustomFet.py @@ -7,30 +7,38 @@ class CustomFet(SwitchFet, FootprintBlock, GeneratorBlock): - def __init__(self, *args: Any, footprint_spec: StringLike = "", - manufacturer_spec: StringLike = "", part_spec: StringLike = "", **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.footprint_spec = self.ArgParameter(footprint_spec) # actual_footprint left to the actual footprint - self.manufacturer_spec = self.ArgParameter(manufacturer_spec) - self.part_spec = self.ArgParameter(part_spec) + def __init__( + self, + *args: Any, + footprint_spec: StringLike = "", + manufacturer_spec: StringLike = "", + part_spec: StringLike = "", + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self.footprint_spec = self.ArgParameter(footprint_spec) # actual_footprint left to the actual footprint + self.manufacturer_spec = self.ArgParameter(manufacturer_spec) + self.part_spec = self.ArgParameter(part_spec) - self.generator_param(self.footprint_spec) + self.generator_param(self.footprint_spec) - # use ideal specs, which can be overridden with refinements - self.assign(self.actual_drain_voltage_rating, Range.all()) - self.assign(self.actual_drain_current_rating, Range.all()) - self.assign(self.actual_gate_voltage_rating, Range.all()) - self.assign(self.actual_gate_drive, Range.zero_to_upper(0)) - self.assign(self.actual_power_rating, Range.all()) - self.assign(self.actual_rds_on, Range.zero_to_upper(0)) - self.assign(self.actual_gate_charge, Range.zero_to_upper(0)) + # use ideal specs, which can be overridden with refinements + self.assign(self.actual_drain_voltage_rating, Range.all()) + self.assign(self.actual_drain_current_rating, Range.all()) + self.assign(self.actual_gate_voltage_rating, Range.all()) + self.assign(self.actual_gate_drive, Range.zero_to_upper(0)) + self.assign(self.actual_power_rating, Range.all()) + self.assign(self.actual_rds_on, Range.zero_to_upper(0)) + self.assign(self.actual_gate_charge, Range.zero_to_upper(0)) - @override - def generate(self) -> None: - self.footprint( - self._standard_footprint().REFDES_PREFIX, self.footprint_spec, - self._standard_footprint()._make_pinning(self, self.get(self.footprint_spec)), - mfr=self.manufacturer_spec, part=self.part_spec, - value=self.part_spec, - datasheet="" - ) + @override + def generate(self) -> None: + self.footprint( + self._standard_footprint().REFDES_PREFIX, + self.footprint_spec, + self._standard_footprint()._make_pinning(self, self.get(self.footprint_spec)), + mfr=self.manufacturer_spec, + part=self.part_spec, + value=self.part_spec, + datasheet="", + ) diff --git a/edg/abstract_parts/DigitalAmplifiers.py b/edg/abstract_parts/DigitalAmplifiers.py index 0d11cdbf9..02cc3a754 100644 --- a/edg/abstract_parts/DigitalAmplifiers.py +++ b/edg/abstract_parts/DigitalAmplifiers.py @@ -10,127 +10,141 @@ class HighSideSwitch(PowerSwitch, KiCadSchematicBlock, GeneratorBlock): - """A high-side FET switch, using a two switch architecture, a main pass PFET with a amplifier NFET to drive its gate. - If clamp_voltage is nonzero, a zener clamp is generated to limit the PFET gate voltage. - The clamp resistor is specified as a ratio from the pull resistance. - - TODO: clamp_voltage should be compared against the actual voltage so the clamp is automatically generated, - but generators don't support link terms (yet?)""" - def __init__(self, pull_resistance: RangeLike = 10*kOhm(tol=0.05), max_rds: FloatLike = 1*Ohm, - frequency: RangeLike = RangeExpr.ZERO, *, - clamp_voltage: RangeLike = RangeExpr.ZERO, clamp_resistance_ratio: FloatLike = 10) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) # amplifier voltage - self.gnd = self.Port(Ground.empty(), [Common]) - - self.control = self.Port(DigitalSink.empty()) - self.output = self.Port(VoltageSource.empty()) - - self.pull_resistance = self.ArgParameter(pull_resistance) - self.max_rds = self.ArgParameter(max_rds) - self.frequency = self.ArgParameter(frequency) - - self.clamp_voltage = self.ArgParameter(clamp_voltage) - self.clamp_resistance_ratio = self.ArgParameter(clamp_resistance_ratio) - self.generator_param(self.clamp_voltage) - - @override - def generate(self) -> None: - super().generate() - - pwr_voltage = self.pwr.link().voltage - pull_resistance = self.pull_resistance - pull_current_max = pwr_voltage.upper() / pull_resistance.lower() - pull_power_max = pwr_voltage.upper() * pwr_voltage.upper() / pull_resistance.lower() - - low_amp_rds_max = pull_resistance.lower() / 1000 - - self.pre = self.Block(SwitchFet.NFet( - drain_voltage=pwr_voltage, - drain_current=(0, pull_current_max), - gate_voltage=(self.control.link().output_thresholds.upper(), self.control.link().voltage.upper()), - rds_on=(0, low_amp_rds_max), # TODO size on turnon time - frequency=self.frequency, - drive_current=self.control.link().current_limits # TODO this is kind of a max drive current - )) - self.pull = self.Block(Resistor( - resistance=pull_resistance, - power=(0, pull_power_max), - voltage=(0, pwr_voltage.upper()) - )) - - clamp_voltage = self.get(self.clamp_voltage) - if clamp_voltage == Range(0, 0): # no clamp - pass_gate_voltage = pwr_voltage - else: - pass_gate_voltage = self.clamp_voltage - - self.drv = self.Block(SwitchFet.PFet( - drain_voltage=pwr_voltage, - drain_current=self.output.link().current_drawn, - gate_voltage=pass_gate_voltage, - rds_on=(0, self.max_rds), - frequency=self.frequency, - drive_current=(-1 * pwr_voltage.lower() / pull_resistance.upper(), - pwr_voltage.lower() / low_amp_rds_max) # TODO simultaneously solve both FETs - )) - - conversions: Dict[str, CircuitPort] = { - 'pwr': VoltageSink( - current_draw=self.output.link().current_drawn - ), - 'output': VoltageSource( - voltage_out=self.pwr.link().voltage, - current_limits=self.drv.actual_drain_current_rating, - ), - 'control': DigitalSink(), # TODO model pullup resistor current - 'gnd': Ground(), - } - - if clamp_voltage == Range(0, 0): # no clamp - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions=conversions) - else: - self.zener = self.Block(ZenerDiode(self.clamp_voltage)) - zlim_resistance = self.pull_resistance / self.clamp_resistance_ratio - zlim_voltage_max = pwr_voltage.upper() - self.clamp_voltage.lower() - zlim_power_max = zlim_voltage_max * zlim_voltage_max / zlim_resistance.lower() - self.zlim = self.Block(Resistor( - resistance=pull_resistance / self.clamp_resistance_ratio, - power=(0, zlim_power_max), - voltage=(0, zlim_voltage_max) - )) - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}_Zener.kicad_sch"), - conversions=conversions) + """A high-side FET switch, using a two switch architecture, a main pass PFET with a amplifier NFET to drive its gate. + If clamp_voltage is nonzero, a zener clamp is generated to limit the PFET gate voltage. + The clamp resistor is specified as a ratio from the pull resistance. + + TODO: clamp_voltage should be compared against the actual voltage so the clamp is automatically generated, + but generators don't support link terms (yet?)""" + + def __init__( + self, + pull_resistance: RangeLike = 10 * kOhm(tol=0.05), + max_rds: FloatLike = 1 * Ohm, + frequency: RangeLike = RangeExpr.ZERO, + *, + clamp_voltage: RangeLike = RangeExpr.ZERO, + clamp_resistance_ratio: FloatLike = 10, + ) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) # amplifier voltage + self.gnd = self.Port(Ground.empty(), [Common]) + + self.control = self.Port(DigitalSink.empty()) + self.output = self.Port(VoltageSource.empty()) + + self.pull_resistance = self.ArgParameter(pull_resistance) + self.max_rds = self.ArgParameter(max_rds) + self.frequency = self.ArgParameter(frequency) + + self.clamp_voltage = self.ArgParameter(clamp_voltage) + self.clamp_resistance_ratio = self.ArgParameter(clamp_resistance_ratio) + self.generator_param(self.clamp_voltage) + + @override + def generate(self) -> None: + super().generate() + + pwr_voltage = self.pwr.link().voltage + pull_resistance = self.pull_resistance + pull_current_max = pwr_voltage.upper() / pull_resistance.lower() + pull_power_max = pwr_voltage.upper() * pwr_voltage.upper() / pull_resistance.lower() + + low_amp_rds_max = pull_resistance.lower() / 1000 + + self.pre = self.Block( + SwitchFet.NFet( + drain_voltage=pwr_voltage, + drain_current=(0, pull_current_max), + gate_voltage=(self.control.link().output_thresholds.upper(), self.control.link().voltage.upper()), + rds_on=(0, low_amp_rds_max), # TODO size on turnon time + frequency=self.frequency, + drive_current=self.control.link().current_limits, # TODO this is kind of a max drive current + ) + ) + self.pull = self.Block( + Resistor(resistance=pull_resistance, power=(0, pull_power_max), voltage=(0, pwr_voltage.upper())) + ) + + clamp_voltage = self.get(self.clamp_voltage) + if clamp_voltage == Range(0, 0): # no clamp + pass_gate_voltage = pwr_voltage + else: + pass_gate_voltage = self.clamp_voltage + + self.drv = self.Block( + SwitchFet.PFet( + drain_voltage=pwr_voltage, + drain_current=self.output.link().current_drawn, + gate_voltage=pass_gate_voltage, + rds_on=(0, self.max_rds), + frequency=self.frequency, + drive_current=( + -1 * pwr_voltage.lower() / pull_resistance.upper(), + pwr_voltage.lower() / low_amp_rds_max, + ), # TODO simultaneously solve both FETs + ) + ) + + conversions: Dict[str, CircuitPort] = { + "pwr": VoltageSink(current_draw=self.output.link().current_drawn), + "output": VoltageSource( + voltage_out=self.pwr.link().voltage, + current_limits=self.drv.actual_drain_current_rating, + ), + "control": DigitalSink(), # TODO model pullup resistor current + "gnd": Ground(), + } + + if clamp_voltage == Range(0, 0): # no clamp + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), conversions=conversions + ) + else: + self.zener = self.Block(ZenerDiode(self.clamp_voltage)) + zlim_resistance = self.pull_resistance / self.clamp_resistance_ratio + zlim_voltage_max = pwr_voltage.upper() - self.clamp_voltage.lower() + zlim_power_max = zlim_voltage_max * zlim_voltage_max / zlim_resistance.lower() + self.zlim = self.Block( + Resistor( + resistance=pull_resistance / self.clamp_resistance_ratio, + power=(0, zlim_power_max), + voltage=(0, zlim_voltage_max), + ) + ) + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}_Zener.kicad_sch"), conversions=conversions + ) class OpenDrainDriver(PowerSwitch, Block): - """NFET configured as an open-drain driver. Potentially useful for voltage translation applications.""" - def __init__(self, max_rds: FloatLike = 1*Ohm, frequency: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() - - self.gnd = self.Port(Ground.empty(), [Common]) - self.control = self.Port(DigitalSink.empty(), [Input]) - self.output = self.Port(DigitalSource.empty(), [Output]) - - self.max_rds = self.ArgParameter(max_rds) - self.frequency = self.ArgParameter(frequency) - - @override - def contents(self) -> None: - super().contents() - - self.drv = self.Block(SwitchFet.NFet( - drain_voltage=self.output.link().voltage, - drain_current=self.output.link().current_drawn, - gate_voltage=self.control.link().voltage, - rds_on=(0, self.max_rds), - frequency=self.frequency, - drive_current=self.control.link().current_limits - )) - self.connect(self.drv.drain.adapt_to(DigitalSource.low_from_supply(self.gnd - )), self.output) - self.connect(self.drv.source.adapt_to(Ground()), self.gnd) - self.connect(self.drv.gate.adapt_to(DigitalSink()), - self.control) + """NFET configured as an open-drain driver. Potentially useful for voltage translation applications.""" + + def __init__(self, max_rds: FloatLike = 1 * Ohm, frequency: RangeLike = RangeExpr.ZERO) -> None: + super().__init__() + + self.gnd = self.Port(Ground.empty(), [Common]) + self.control = self.Port(DigitalSink.empty(), [Input]) + self.output = self.Port(DigitalSource.empty(), [Output]) + + self.max_rds = self.ArgParameter(max_rds) + self.frequency = self.ArgParameter(frequency) + + @override + def contents(self) -> None: + super().contents() + + self.drv = self.Block( + SwitchFet.NFet( + drain_voltage=self.output.link().voltage, + drain_current=self.output.link().current_drawn, + gate_voltage=self.control.link().voltage, + rds_on=(0, self.max_rds), + frequency=self.frequency, + drive_current=self.control.link().current_limits, + ) + ) + self.connect(self.drv.drain.adapt_to(DigitalSource.low_from_supply(self.gnd)), self.output) + self.connect(self.drv.source.adapt_to(Ground()), self.gnd) + self.connect(self.drv.gate.adapt_to(DigitalSink()), self.control) diff --git a/edg/abstract_parts/DigitalIsolator.py b/edg/abstract_parts/DigitalIsolator.py index 764957baf..17af92367 100644 --- a/edg/abstract_parts/DigitalIsolator.py +++ b/edg/abstract_parts/DigitalIsolator.py @@ -6,29 +6,34 @@ @abstract_block class DigitalIsolator(Interface, GeneratorBlock): - """Multichannel digital isolator, shifts logic signals between different logic voltages - and isolation domains. Supports arbitrary channels in either direction, but it needs to - map down to a single chip (or be multipacked). - in_a -> out_b, and in_b -> out_a must each have the same array elements, which is how - channels will be matched to pins.""" - def __init__(self) -> None: - super().__init__() - self.pwr_a = self.Port(VoltageSink.empty()) - self.gnd_a = self.Port(Ground.empty()) - self.in_a = self.Port(Vector(DigitalSink.empty()), optional=True) - self.out_a = self.Port(Vector(DigitalSource.empty()), optional=True) + """Multichannel digital isolator, shifts logic signals between different logic voltages + and isolation domains. Supports arbitrary channels in either direction, but it needs to + map down to a single chip (or be multipacked). + in_a -> out_b, and in_b -> out_a must each have the same array elements, which is how + channels will be matched to pins.""" - self.pwr_b = self.Port(VoltageSink.empty()) - self.gnd_b = self.Port(Ground.empty()) - self.in_b = self.Port(Vector(DigitalSink.empty()), optional=True) - self.out_b = self.Port(Vector(DigitalSource.empty()), optional=True) + def __init__(self) -> None: + super().__init__() + self.pwr_a = self.Port(VoltageSink.empty()) + self.gnd_a = self.Port(Ground.empty()) + self.in_a = self.Port(Vector(DigitalSink.empty()), optional=True) + self.out_a = self.Port(Vector(DigitalSource.empty()), optional=True) - self.generator_param(self.in_a.requested(), self.out_b.requested(), self.in_b.requested(), self.out_a.requested()) + self.pwr_b = self.Port(VoltageSink.empty()) + self.gnd_b = self.Port(Ground.empty()) + self.in_b = self.Port(Vector(DigitalSink.empty()), optional=True) + self.out_b = self.Port(Vector(DigitalSource.empty()), optional=True) - @override - def generate(self) -> None: # validity checks - super().generate() - assert self.get(self.in_a.requested()) == self.get(self.out_b.requested()), \ - "in_a requested and out_b requested must be equal" - assert self.get(self.in_b.requested()) == self.get(self.out_a.requested()), \ - "in_b requested and out_a requested must be equal" + self.generator_param( + self.in_a.requested(), self.out_b.requested(), self.in_b.requested(), self.out_a.requested() + ) + + @override + def generate(self) -> None: # validity checks + super().generate() + assert self.get(self.in_a.requested()) == self.get( + self.out_b.requested() + ), "in_a requested and out_b requested must be equal" + assert self.get(self.in_b.requested()) == self.get( + self.out_a.requested() + ), "in_b requested and out_a requested must be equal" diff --git a/edg/abstract_parts/DummyDevices.py b/edg/abstract_parts/DummyDevices.py index b5b4ddc15..7bd0ab83a 100644 --- a/edg/abstract_parts/DummyDevices.py +++ b/edg/abstract_parts/DummyDevices.py @@ -5,211 +5,191 @@ class DummyPassive(DummyDevice): - def __init__(self) -> None: - super().__init__() - self.io = self.Port(Passive(), [InOut]) + def __init__(self) -> None: + super().__init__() + self.io = self.Port(Passive(), [InOut]) class DummyGround(DummyDevice): - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground(), [Common, InOut]) + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground(), [Common, InOut]) class DummyVoltageSource(DummyDevice): - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL) -> None: - super().__init__() + def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, current_limits: RangeLike = RangeExpr.ALL) -> None: + super().__init__() - self.pwr = self.Port(VoltageSource( - voltage_out=voltage_out, - current_limits=current_limits - ), [Power, InOut]) + self.pwr = self.Port(VoltageSource(voltage_out=voltage_out, current_limits=current_limits), [Power, InOut]) - self.current_drawn = self.Parameter(RangeExpr(self.pwr.link().current_drawn)) - self.voltage_limits = self.Parameter(RangeExpr(self.pwr.link().voltage_limits)) + self.current_drawn = self.Parameter(RangeExpr(self.pwr.link().current_drawn)) + self.voltage_limits = self.Parameter(RangeExpr(self.pwr.link().voltage_limits)) class DummyVoltageSink(DummyDevice): - def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() + def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL, current_draw: RangeLike = RangeExpr.ZERO) -> None: + super().__init__() - self.pwr = self.Port(VoltageSink( - voltage_limits=voltage_limit, - current_draw=current_draw - ), [Power, InOut]) - self.voltage = self.Parameter(RangeExpr(self.pwr.link().voltage)) - self.current_limits = self.Parameter(RangeExpr(self.pwr.link().current_limits)) + self.pwr = self.Port(VoltageSink(voltage_limits=voltage_limit, current_draw=current_draw), [Power, InOut]) + self.voltage = self.Parameter(RangeExpr(self.pwr.link().voltage)) + self.current_limits = self.Parameter(RangeExpr(self.pwr.link().current_limits)) class DummyDigitalSource(DummyDevice): - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL) -> None: - super().__init__() + def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, current_limits: RangeLike = RangeExpr.ALL) -> None: + super().__init__() - self.io = self.Port(DigitalSource( - voltage_out=voltage_out, - current_limits=current_limits - ), [InOut]) + self.io = self.Port(DigitalSource(voltage_out=voltage_out, current_limits=current_limits), [InOut]) class DummyDigitalSink(DummyDevice): - def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() + def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL, current_draw: RangeLike = RangeExpr.ZERO) -> None: + super().__init__() - self.io = self.Port(DigitalSink( - voltage_limits=voltage_limit, - current_draw=current_draw - ), [InOut]) + self.io = self.Port(DigitalSink(voltage_limits=voltage_limit, current_draw=current_draw), [InOut]) class DummyAnalogSource(DummyDevice): - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, - signal_out: RangeLike = RangeExpr.EMPTY, - current_limits: RangeLike = RangeExpr.ALL, - impedance: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() - - self.io = self.Port(AnalogSource( - voltage_out=voltage_out, - signal_out=signal_out, - current_limits=current_limits, - impedance=impedance - ), [InOut]) + def __init__( + self, + voltage_out: RangeLike = RangeExpr.ZERO, + signal_out: RangeLike = RangeExpr.EMPTY, + current_limits: RangeLike = RangeExpr.ALL, + impedance: RangeLike = RangeExpr.ZERO, + ) -> None: + super().__init__() + + self.io = self.Port( + AnalogSource( + voltage_out=voltage_out, signal_out=signal_out, current_limits=current_limits, impedance=impedance + ), + [InOut], + ) class DummyAnalogSink(DummyDevice): - def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL, - signal_limit: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO, - impedance: RangeLike = RangeExpr.INF) -> None: - super().__init__() - - self.io = self.Port(AnalogSink( - voltage_limits=voltage_limit, - signal_limits=signal_limit, - current_draw=current_draw, - impedance=impedance - ), [InOut]) + def __init__( + self, + voltage_limit: RangeLike = RangeExpr.ALL, + signal_limit: RangeLike = RangeExpr.ALL, + current_draw: RangeLike = RangeExpr.ZERO, + impedance: RangeLike = RangeExpr.INF, + ) -> None: + super().__init__() + + self.io = self.Port( + AnalogSink( + voltage_limits=voltage_limit, signal_limits=signal_limit, current_draw=current_draw, impedance=impedance + ), + [InOut], + ) class ForcedVoltageCurrentDraw(DummyDevice, NetBlock): - """Forces some input current draw regardless of the output's actual current draw value""" - def __init__(self, forced_current_draw: RangeLike) -> None: - super().__init__() + """Forces some input current draw regardless of the output's actual current draw value""" + + def __init__(self, forced_current_draw: RangeLike) -> None: + super().__init__() - self.pwr_in = self.Port(VoltageSink( - current_draw=forced_current_draw, - voltage_limits=RangeExpr.ALL - ), [Input]) + self.pwr_in = self.Port(VoltageSink(current_draw=forced_current_draw, voltage_limits=RangeExpr.ALL), [Input]) - self.pwr_out = self.Port(VoltageSource( - voltage_out=self.pwr_in.link().voltage, - current_limits=RangeExpr.ALL - ), [Output]) + self.pwr_out = self.Port( + VoltageSource(voltage_out=self.pwr_in.link().voltage, current_limits=RangeExpr.ALL), [Output] + ) class ForcedVoltageCurrentLimit(DummyDevice, NetBlock): - """Forces some output current limit, which should be tighter than the input's actual current draw.""" - def __init__(self, forced_current_limit: RangeLike) -> None: - super().__init__() + """Forces some output current limit, which should be tighter than the input's actual current draw.""" - self.pwr_in = self.Port(VoltageSink( - current_draw=RangeExpr(), - voltage_limits=RangeExpr.ALL - ), [Input]) + def __init__(self, forced_current_limit: RangeLike) -> None: + super().__init__() - self.pwr_out = self.Port(VoltageSource( - voltage_out=self.pwr_in.link().voltage, - current_limits=forced_current_limit - ), [Output]) + self.pwr_in = self.Port(VoltageSink(current_draw=RangeExpr(), voltage_limits=RangeExpr.ALL), [Input]) - self.assign(self.pwr_in.current_draw, self.pwr_out.link().current_drawn) + self.pwr_out = self.Port( + VoltageSource(voltage_out=self.pwr_in.link().voltage, current_limits=forced_current_limit), [Output] + ) + + self.assign(self.pwr_in.current_draw, self.pwr_out.link().current_drawn) class ForcedVoltage(DummyDevice, NetBlock): - """Forces some voltage on the output regardless of the input's actual voltage. - Current draw is passed through unchanged.""" - def __init__(self, forced_voltage: RangeLike) -> None: - super().__init__() + """Forces some voltage on the output regardless of the input's actual voltage. + Current draw is passed through unchanged.""" + + def __init__(self, forced_voltage: RangeLike) -> None: + super().__init__() - self.pwr_in = self.Port(VoltageSink( - current_draw=RangeExpr() - ), [Input]) + self.pwr_in = self.Port(VoltageSink(current_draw=RangeExpr()), [Input]) - self.pwr_out = self.Port(VoltageSource( - voltage_out=forced_voltage - ), [Output]) + self.pwr_out = self.Port(VoltageSource(voltage_out=forced_voltage), [Output]) - self.assign(self.pwr_in.current_draw, self.pwr_out.link().current_drawn) + self.assign(self.pwr_in.current_draw, self.pwr_out.link().current_drawn) class ForcedVoltageCurrent(DummyDevice, NetBlock): - """Forces some voltage and current on the output regardless of the input's actual parameters.""" - def __init__(self, forced_voltage: RangeLike, forced_current: RangeLike) -> None: - super().__init__() + """Forces some voltage and current on the output regardless of the input's actual parameters.""" + + def __init__(self, forced_voltage: RangeLike, forced_current: RangeLike) -> None: + super().__init__() - self.pwr_in = self.Port(VoltageSink( - current_draw=forced_current - ), [Input]) + self.pwr_in = self.Port(VoltageSink(current_draw=forced_current), [Input]) - self.pwr_out = self.Port(VoltageSource( - voltage_out=forced_voltage - ), [Output]) + self.pwr_out = self.Port(VoltageSource(voltage_out=forced_voltage), [Output]) class ForcedAnalogVoltage(DummyDevice, NetBlock): - def __init__(self, forced_voltage: RangeLike = RangeExpr()) -> None: - super().__init__() + def __init__(self, forced_voltage: RangeLike = RangeExpr()) -> None: + super().__init__() - self.signal_in = self.Port(AnalogSink( - current_draw=RangeExpr() - ), [Input]) + self.signal_in = self.Port(AnalogSink(current_draw=RangeExpr()), [Input]) - self.signal_out = self.Port(AnalogSource( - voltage_out=forced_voltage, - signal_out=self.signal_in.link().signal - ), [Output]) + self.signal_out = self.Port( + AnalogSource(voltage_out=forced_voltage, signal_out=self.signal_in.link().signal), [Output] + ) - self.assign(self.signal_in.current_draw, self.signal_out.link().current_drawn) + self.assign(self.signal_in.current_draw, self.signal_out.link().current_drawn) class ForcedAnalogSignal(KiCadImportableBlock, DummyDevice, NetBlock): - def __init__(self, forced_signal: RangeLike = RangeExpr()) -> None: - super().__init__() + def __init__(self, forced_signal: RangeLike = RangeExpr()) -> None: + super().__init__() - self.signal_in = self.Port(AnalogSink( - current_draw=RangeExpr() - ), [Input]) + self.signal_in = self.Port(AnalogSink(current_draw=RangeExpr()), [Input]) - self.signal_out = self.Port(AnalogSource( - voltage_out=self.signal_in.link().voltage, - signal_out=forced_signal, - current_limits=self.signal_in.link().current_limits - ), [Output]) + self.signal_out = self.Port( + AnalogSource( + voltage_out=self.signal_in.link().voltage, + signal_out=forced_signal, + current_limits=self.signal_in.link().current_limits, + ), + [Output], + ) - self.assign(self.signal_in.current_draw, self.signal_out.link().current_drawn) + self.assign(self.signal_in.current_draw, self.signal_out.link().current_drawn) - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'edg_importable:Adapter' - return {'1': self.signal_in, '2': self.signal_out} + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name == "edg_importable:Adapter" + return {"1": self.signal_in, "2": self.signal_out} class ForcedDigitalSinkCurrentDraw(DummyDevice, NetBlock): - def __init__(self, forced_current_draw: RangeLike = RangeExpr()) -> None: - super().__init__() - - self.pwr_in = self.Port(DigitalSink( - current_draw=forced_current_draw, - voltage_limits=RangeExpr.ALL, - input_thresholds=RangeExpr.EMPTY - ), [Input]) - - self.pwr_out = self.Port(DigitalSource( - voltage_out=self.pwr_in.link().voltage, - current_limits=RangeExpr.ALL, - output_thresholds=self.pwr_in.link().output_thresholds - ), [Output]) + def __init__(self, forced_current_draw: RangeLike = RangeExpr()) -> None: + super().__init__() + + self.pwr_in = self.Port( + DigitalSink( + current_draw=forced_current_draw, voltage_limits=RangeExpr.ALL, input_thresholds=RangeExpr.EMPTY + ), + [Input], + ) + + self.pwr_out = self.Port( + DigitalSource( + voltage_out=self.pwr_in.link().voltage, + current_limits=RangeExpr.ALL, + output_thresholds=self.pwr_in.link().output_thresholds, + ), + [Output], + ) diff --git a/edg/abstract_parts/ESeriesUtil.py b/edg/abstract_parts/ESeriesUtil.py index 68cd6d53f..c30cfa575 100644 --- a/edg/abstract_parts/ESeriesUtil.py +++ b/edg/abstract_parts/ESeriesUtil.py @@ -7,267 +7,455 @@ from ..electronics_model import * -SeriesDefaultType = TypeVar('SeriesDefaultType') +SeriesDefaultType = TypeVar("SeriesDefaultType") class ESeriesUtil: - """Helper methods for working with the E series of preferred numbers.""" - @staticmethod - def zigzag_range(start: int, end: int) -> Sequence[int]: - if start >= end: - return [] - - center = int((start + end - 1) / 2) - lower = list(range(start, center)) - upper = list(range(center + 1, end)) - output = [center] - - while lower or upper: - if lower: - output.append(lower.pop(0)) - if upper: - output.append(upper.pop(0)) - - return output - - @staticmethod - def round_sig(x: float, sig: int) -> float: - # this prevents floating point weirdness, eg 819.999 - return round(x, sig-int(math.floor(math.log10(abs(x))))-1) - - @classmethod - def choose_preferred_number(cls, within: Range, series: Sequence[float], tolerance: float) -> \ - Optional[float]: - if within.lower == 0: - lower_pow10 = -6 # arbitrarily micro-level, which is common for capacitors and inductors - else: - lower_pow10 = math.floor(math.log10(within.lower)) - - if within.upper == float('inf'): - upper_pow10 = lower_pow10 + 2 # give it two decades so it can select the exact power of 10 ('E1 series') - else: - upper_pow10 = math.ceil(math.log10(within.upper)) - - powers = cls.zigzag_range(lower_pow10, upper_pow10) # prefer the center power first, then zigzag away from it - # TODO given the tolerance we can actually bound this further - - for value in series: - for power in powers: - pow10_mult = math.pow(10, power) - value_mult = cls.round_sig(value * pow10_mult, cls.ROUND_DIGITS) - if Range.from_tolerance(value_mult, tolerance) in within: - return value_mult - - return None - - ROUND_DIGITS = 5 - - SERIES_MAX = 192 - - E24_DIFF = { # series as difference from prior series - 3: [1.0, 2.2, 4.7], - 6: [1.5, 3.3, 6.8], - 12: [1.2, 1.8, 2.7, 3.9, 5.6, 8.2], - 24: [1.1, 1.3, 1.6, 2.0, 2.4, 3.0, 3.6, 4.3, 5.1, 6.2, 7.5, 9.1], - } - - E192_DIFF = { - 48: [1.00, 1.05, 1.10, 1.15, 1.21, 1.27, 1.33, 1.40, 1.47, 1.54, 1.62, 1.69, - 1.78, 1.87, 1.96, 2.05, 2.15, 2.26, 2.37, 2.49, 2.61, 2.74, 2.87, 3.01, - 3.16, 3.32, 3.48, 3.65, 3.83, 4.02, 4.22, 4.42, 4.64, 4.87, 5.11, 5.36, - 5.62, 5.90, 6.19, 6.49, 6.81, 7.15, 7.50, 7.87, 8.25, 8.66, 9.09, 9.53], - 96: [1.02, 1.07, 1.13, 1.18, 1.24, 1.30, 1.37, 1.43, 1.50, 1.58, 1.65, 1.74, - 1.82, 1.91, 2.00, 2.10, 2.21, 2.32, 2.43, 2.55, 2.67, 2.80, 2.94, 3.09, - 3.24, 3.40, 3.57, 3.74, 3.92, 4.12, 4.32, 4.53, 4.75, 4.99, 5.23, 5.49, - 5.76, 6.04, 6.34, 6.65, 6.98, 7.32, 7.68, 8.06, 8.45, 8.87, 9.31, 9.76], - 192: [1.01, 1.04, 1.06, 1.09, 1.11, 1.14, 1.17, 1.20, 1.23, 1.26, 1.29, 1.32, - 1.35, 1.38, 1.42, 1.45, 1.49, 1.52, 1.56, 1.60, 1.64, 1.67, 1.72, 1.76, - 1.80, 1.84, 1.89, 1.93, 1.98, 2.03, 2.08, 2.13, 2.18, 2.23, 2.29, 2.34, - 2.40, 2.46, 2.52, 2.58, 2.64, 2.71, 2.77, 2.84, 2.91, 2.98, 3.05, 3.12, - 3.20, 3.28, 3.36, 3.44, 3.52, 3.61, 3.70, 3.79, 3.88, 3.97, 4.07, 4.17, - 4.27, 4.37, 4.48, 4.59, 4.70, 4.81, 4.93, 5.05, 5.17, 5.30, 5.42, 5.56, - 5.69, 5.83, 5.97, 6.12, 6.26, 6.42, 6.57, 6.73, 6.90, 7.06, 7.23, 7.41, - 7.59, 7.77, 7.96, 8.16, 8.35, 8.56, 8.76, 8.98, 9.20, 9.42, 9.65, 9.88], - } - - SERIES = { # whole series in zigzag order - 3: list(itertools.chain(E24_DIFF[3])), - 6: list(itertools.chain(E24_DIFF[3], E24_DIFF[6])), - 12: list(itertools.chain(E24_DIFF[3], E24_DIFF[6], E24_DIFF[12])), - 24: list(itertools.chain(E24_DIFF[3], E24_DIFF[6], E24_DIFF[12], E24_DIFF[24])), - - # These are E192 without the E24 series - 48: list(itertools.chain(E192_DIFF[48])), - 96: list(itertools.chain(E192_DIFF[48], E192_DIFF[96])), - 192: list(itertools.chain(E192_DIFF[48], E192_DIFF[96], E192_DIFF[192])), - } - - # reverse mapping of value to series, reverse SERIES so lower series preferred - VALUE_SERIES = {v: k for k, series in reversed(SERIES.items()) for v in series} - - @classmethod - @overload - def series_of(cls, value: float) -> Optional[int]: ... - @classmethod - @overload - def series_of(cls, value: float, *, default: SeriesDefaultType) -> Union[int, SeriesDefaultType]: ... - - @classmethod - def series_of(cls, value: float, *, default: Optional[SeriesDefaultType] = None) -> Union[None, int, SeriesDefaultType]: - """Returns the E-series that contains the given value, or None if not found. - Performs limited rounding to account for floating point issues.""" - if value <= 0: - return default - normalized_value = value * math.pow(10, -math.floor(math.log10(value))) - return cls.VALUE_SERIES.get(cls.round_sig(normalized_value, cls.ROUND_DIGITS), default) - - -ESeriesRatioValueType = TypeVar('ESeriesRatioValueType', bound='ESeriesRatioValue[Any]') + """Helper methods for working with the E series of preferred numbers.""" + + @staticmethod + def zigzag_range(start: int, end: int) -> Sequence[int]: + if start >= end: + return [] + + center = int((start + end - 1) / 2) + lower = list(range(start, center)) + upper = list(range(center + 1, end)) + output = [center] + + while lower or upper: + if lower: + output.append(lower.pop(0)) + if upper: + output.append(upper.pop(0)) + + return output + + @staticmethod + def round_sig(x: float, sig: int) -> float: + # this prevents floating point weirdness, eg 819.999 + return round(x, sig - int(math.floor(math.log10(abs(x)))) - 1) + + @classmethod + def choose_preferred_number(cls, within: Range, series: Sequence[float], tolerance: float) -> Optional[float]: + if within.lower == 0: + lower_pow10 = -6 # arbitrarily micro-level, which is common for capacitors and inductors + else: + lower_pow10 = math.floor(math.log10(within.lower)) + + if within.upper == float("inf"): + upper_pow10 = lower_pow10 + 2 # give it two decades so it can select the exact power of 10 ('E1 series') + else: + upper_pow10 = math.ceil(math.log10(within.upper)) + + powers = cls.zigzag_range(lower_pow10, upper_pow10) # prefer the center power first, then zigzag away from it + # TODO given the tolerance we can actually bound this further + + for value in series: + for power in powers: + pow10_mult = math.pow(10, power) + value_mult = cls.round_sig(value * pow10_mult, cls.ROUND_DIGITS) + if Range.from_tolerance(value_mult, tolerance) in within: + return value_mult + + return None + + ROUND_DIGITS = 5 + + SERIES_MAX = 192 + + E24_DIFF = { # series as difference from prior series + 3: [1.0, 2.2, 4.7], + 6: [1.5, 3.3, 6.8], + 12: [1.2, 1.8, 2.7, 3.9, 5.6, 8.2], + 24: [1.1, 1.3, 1.6, 2.0, 2.4, 3.0, 3.6, 4.3, 5.1, 6.2, 7.5, 9.1], + } + + E192_DIFF = { + 48: [ + 1.00, + 1.05, + 1.10, + 1.15, + 1.21, + 1.27, + 1.33, + 1.40, + 1.47, + 1.54, + 1.62, + 1.69, + 1.78, + 1.87, + 1.96, + 2.05, + 2.15, + 2.26, + 2.37, + 2.49, + 2.61, + 2.74, + 2.87, + 3.01, + 3.16, + 3.32, + 3.48, + 3.65, + 3.83, + 4.02, + 4.22, + 4.42, + 4.64, + 4.87, + 5.11, + 5.36, + 5.62, + 5.90, + 6.19, + 6.49, + 6.81, + 7.15, + 7.50, + 7.87, + 8.25, + 8.66, + 9.09, + 9.53, + ], + 96: [ + 1.02, + 1.07, + 1.13, + 1.18, + 1.24, + 1.30, + 1.37, + 1.43, + 1.50, + 1.58, + 1.65, + 1.74, + 1.82, + 1.91, + 2.00, + 2.10, + 2.21, + 2.32, + 2.43, + 2.55, + 2.67, + 2.80, + 2.94, + 3.09, + 3.24, + 3.40, + 3.57, + 3.74, + 3.92, + 4.12, + 4.32, + 4.53, + 4.75, + 4.99, + 5.23, + 5.49, + 5.76, + 6.04, + 6.34, + 6.65, + 6.98, + 7.32, + 7.68, + 8.06, + 8.45, + 8.87, + 9.31, + 9.76, + ], + 192: [ + 1.01, + 1.04, + 1.06, + 1.09, + 1.11, + 1.14, + 1.17, + 1.20, + 1.23, + 1.26, + 1.29, + 1.32, + 1.35, + 1.38, + 1.42, + 1.45, + 1.49, + 1.52, + 1.56, + 1.60, + 1.64, + 1.67, + 1.72, + 1.76, + 1.80, + 1.84, + 1.89, + 1.93, + 1.98, + 2.03, + 2.08, + 2.13, + 2.18, + 2.23, + 2.29, + 2.34, + 2.40, + 2.46, + 2.52, + 2.58, + 2.64, + 2.71, + 2.77, + 2.84, + 2.91, + 2.98, + 3.05, + 3.12, + 3.20, + 3.28, + 3.36, + 3.44, + 3.52, + 3.61, + 3.70, + 3.79, + 3.88, + 3.97, + 4.07, + 4.17, + 4.27, + 4.37, + 4.48, + 4.59, + 4.70, + 4.81, + 4.93, + 5.05, + 5.17, + 5.30, + 5.42, + 5.56, + 5.69, + 5.83, + 5.97, + 6.12, + 6.26, + 6.42, + 6.57, + 6.73, + 6.90, + 7.06, + 7.23, + 7.41, + 7.59, + 7.77, + 7.96, + 8.16, + 8.35, + 8.56, + 8.76, + 8.98, + 9.20, + 9.42, + 9.65, + 9.88, + ], + } + + SERIES = { # whole series in zigzag order + 3: list(itertools.chain(E24_DIFF[3])), + 6: list(itertools.chain(E24_DIFF[3], E24_DIFF[6])), + 12: list(itertools.chain(E24_DIFF[3], E24_DIFF[6], E24_DIFF[12])), + 24: list(itertools.chain(E24_DIFF[3], E24_DIFF[6], E24_DIFF[12], E24_DIFF[24])), + # These are E192 without the E24 series + 48: list(itertools.chain(E192_DIFF[48])), + 96: list(itertools.chain(E192_DIFF[48], E192_DIFF[96])), + 192: list(itertools.chain(E192_DIFF[48], E192_DIFF[96], E192_DIFF[192])), + } + + # reverse mapping of value to series, reverse SERIES so lower series preferred + VALUE_SERIES = {v: k for k, series in reversed(SERIES.items()) for v in series} + + @classmethod + @overload + def series_of(cls, value: float) -> Optional[int]: ... + @classmethod + @overload + def series_of(cls, value: float, *, default: SeriesDefaultType) -> Union[int, SeriesDefaultType]: ... + + @classmethod + def series_of( + cls, value: float, *, default: Optional[SeriesDefaultType] = None + ) -> Union[None, int, SeriesDefaultType]: + """Returns the E-series that contains the given value, or None if not found. + Performs limited rounding to account for floating point issues.""" + if value <= 0: + return default + normalized_value = value * math.pow(10, -math.floor(math.log10(value))) + return cls.VALUE_SERIES.get(cls.round_sig(normalized_value, cls.ROUND_DIGITS), default) + + +ESeriesRatioValueType = TypeVar("ESeriesRatioValueType", bound="ESeriesRatioValue[Any]") + + class ESeriesRatioValue(Generic[ESeriesRatioValueType]): - """Abstract base class for the calculated output value for a resistor ... thing. - Yes, not too descriptive, but example applications are: - - resistive divider: ratio and impedance - - non-inverting amplifier: amplification factor and impedance - - really anything that takes two E-series values and where there isn't - a nice closed-form solution so we test-and-check across decades - """ - @staticmethod - @abstractmethod - def from_resistors(r1: Range, r2: Range) -> ESeriesRatioValueType: - """Calculates the range of outputs possible given input range of resistors.""" - ... - - @abstractmethod - def initial_test_decades(self) -> Tuple[int, int]: - """Returns the initial decades to test that can satisfy this spec.""" - ... - - @abstractmethod - def distance_to(self, spec: ESeriesRatioValueType) -> List[float]: - """Returns a distance vector to the spec, or the empty list if satisfying the spec""" - ... - - @abstractmethod - def intersects(self, spec: ESeriesRatioValueType) -> bool: - """Return whether this intersects with some spec - whether some subset of the resistors - can potentially satisfy some spec""" - ... + """Abstract base class for the calculated output value for a resistor ... thing. + Yes, not too descriptive, but example applications are: + - resistive divider: ratio and impedance + - non-inverting amplifier: amplification factor and impedance + - really anything that takes two E-series values and where there isn't + a nice closed-form solution so we test-and-check across decades + """ + + @staticmethod + @abstractmethod + def from_resistors(r1: Range, r2: Range) -> ESeriesRatioValueType: + """Calculates the range of outputs possible given input range of resistors.""" + ... + + @abstractmethod + def initial_test_decades(self) -> Tuple[int, int]: + """Returns the initial decades to test that can satisfy this spec.""" + ... + + @abstractmethod + def distance_to(self, spec: ESeriesRatioValueType) -> List[float]: + """Returns a distance vector to the spec, or the empty list if satisfying the spec""" + ... + + @abstractmethod + def intersects(self, spec: ESeriesRatioValueType) -> bool: + """Return whether this intersects with some spec - whether some subset of the resistors + can potentially satisfy some spec""" + ... class ESeriesRatioUtil(Generic[ESeriesRatioValueType]): - """Base class for an algorithm that searches pairs of E-series numbers - to get some desired output (eg, ratio and impedance for a resistive divider). - The output calculations are determined by the value_type class. - Calculation of the initial decade (for both values) and which way to shift decades - after scanning an entire decade can also be overridden - - Within a decade, this prefers combinations of smaller E-series before moving on, - eg a satisfying E3 pair is preferred and returned, even if there is a closer E6 pair. - This has no concept of a distance metric when the spec is satisfied. - - The code below is defined in terms of resistors, but this can be used with anything - that uses the E-series. - - Series should be the zero decade, in the range of [1, 10) - """ - class NoMatchException(Exception): - pass - - def __init__(self, series: List[float], tolerance: float, value_type: Type[ESeriesRatioValueType]): - self.series = series - self.tolerance = tolerance - self.value_type = value_type - - def _no_result_error(self, best_values: Tuple[float, float], best: ESeriesRatioValueType, - target: ESeriesRatioValueType) -> Exception: - """Given the best tested result and a target, generate an exception to throw. - This should not throw the exception, only generate it.""" - return self.NoMatchException(f"No ratio found for target {target}, " - f"best: {best}, with values {best_values}") - - @staticmethod - def _generate_e_series_product(series: List[float], r1_decade: int, r2_decade: int) -> List[Tuple[float, float]]: - """Returns the ordered / sorted cartesian product of all possible pairs of values for the requested decade. - The output is ordered such that pairs containing numbers earlier in the series comes first, - so in effect lower E-series combinations are preferred, assuming the series is ordered in a - zig-zag fashion.""" - r1_series = [ESeriesUtil.round_sig(elt * (10 ** r1_decade), ESeriesUtil.ROUND_DIGITS) - for elt in series] - r2_series = [ESeriesUtil.round_sig(elt * (10 ** r2_decade), ESeriesUtil.ROUND_DIGITS) - for elt in series] - out = [] - assert len(r1_series) == len(r2_series), "algorithm depends on same series length" - for index_max in range(len(r1_series)): - for index_other in range(index_max): - out.append((r1_series[index_max], r2_series[index_other])) - out.append((r1_series[index_other], r2_series[index_max])) - out.append((r1_series[index_max], r2_series[index_max])) - - return out - - def find(self, target: ESeriesRatioValueType) -> Tuple[float, float]: - """Find a pair of R1, R2 that satisfies the target.""" - initial = target.initial_test_decades() - search_queue = deque([initial]) - searched_decades = {initial} # tracks everything that has been on the search queue - best = None - - while search_queue: - r1r2_decade = search_queue.popleft() - product = self._generate_e_series_product(self.series, - r1r2_decade[0], r1r2_decade[1]) - - for (r1, r2) in product: - output = self.value_type.from_resistors(Range.from_tolerance(r1, self.tolerance), - Range.from_tolerance(r2, self.tolerance)) - output_dist = output.distance_to(target) - - if best is None or output_dist < best[2]: - best = ((r1, r2), output, output_dist) - if not output_dist: - break - - assert best is not None - if not best[2]: # distance vector empty = satisfying - return best[0] - else: - next_decades = self._get_next_decades(r1r2_decade, target) - for next_decade in next_decades: - if next_decade not in searched_decades and -15 < next_decade[0] < 15 and -15 < next_decade[1] < 15: - searched_decades.add(next_decade) - search_queue.append(next_decade) - - # if it gets here, the search queue has been exhausted without a result - assert best is not None - raise self._no_result_error(best[0], best[1], target) - - def _get_next_decades(self, decade: Tuple[int, int], target: ESeriesRatioValueType) -> List[Tuple[int, int]]: - """If the target was not found scanning the entire decade, this is called to determine next decades to search. - This is passed in the current decade and the target. - - Returns a list of decades to search, in order. Internally the search algorithm deduplicates decades. - This is called for every decade, and results are appended to the end of the search queue after deduplication. - - The default algorithm returns all adjacent combinations of decades where the output - intersects the target. + """Base class for an algorithm that searches pairs of E-series numbers + to get some desired output (eg, ratio and impedance for a resistive divider). + The output calculations are determined by the value_type class. + Calculation of the initial decade (for both values) and which way to shift decades + after scanning an entire decade can also be overridden + + Within a decade, this prefers combinations of smaller E-series before moving on, + eg a satisfying E3 pair is preferred and returned, even if there is a closer E6 pair. + This has no concept of a distance metric when the spec is satisfied. + + The code below is defined in terms of resistors, but this can be used with anything + that uses the E-series. + + Series should be the zero decade, in the range of [1, 10) """ - def range_of_decade(range_decade: int) -> Range: - """Given a decade, return the range of possible values - for example, decade 0 - would mean 1.0, 2.2, 4.7 and would return a range of (1, 10).""" - return Range(10 ** range_decade, 10 ** (range_decade + 1)) - - test_decades = [ - # adjustments shifting both decades in the same direction - (decade[0] - 1, decade[1] - 1), - (decade[0] + 1, decade[1] + 1), - # adjustments shifting decades independently - (decade[0] - 1, decade[1]), - (decade[0], decade[1] + 1), - (decade[0] + 1, decade[1]), - (decade[0], decade[1] - 1), - ] - - new_decades = [decade for decade in test_decades - if self.value_type.from_resistors(range_of_decade(decade[0]), - range_of_decade(decade[1])).intersects(target)] - return new_decades + + class NoMatchException(Exception): + pass + + def __init__(self, series: List[float], tolerance: float, value_type: Type[ESeriesRatioValueType]): + self.series = series + self.tolerance = tolerance + self.value_type = value_type + + def _no_result_error( + self, best_values: Tuple[float, float], best: ESeriesRatioValueType, target: ESeriesRatioValueType + ) -> Exception: + """Given the best tested result and a target, generate an exception to throw. + This should not throw the exception, only generate it.""" + return self.NoMatchException(f"No ratio found for target {target}, " f"best: {best}, with values {best_values}") + + @staticmethod + def _generate_e_series_product(series: List[float], r1_decade: int, r2_decade: int) -> List[Tuple[float, float]]: + """Returns the ordered / sorted cartesian product of all possible pairs of values for the requested decade. + The output is ordered such that pairs containing numbers earlier in the series comes first, + so in effect lower E-series combinations are preferred, assuming the series is ordered in a + zig-zag fashion.""" + r1_series = [ESeriesUtil.round_sig(elt * (10**r1_decade), ESeriesUtil.ROUND_DIGITS) for elt in series] + r2_series = [ESeriesUtil.round_sig(elt * (10**r2_decade), ESeriesUtil.ROUND_DIGITS) for elt in series] + out = [] + assert len(r1_series) == len(r2_series), "algorithm depends on same series length" + for index_max in range(len(r1_series)): + for index_other in range(index_max): + out.append((r1_series[index_max], r2_series[index_other])) + out.append((r1_series[index_other], r2_series[index_max])) + out.append((r1_series[index_max], r2_series[index_max])) + + return out + + def find(self, target: ESeriesRatioValueType) -> Tuple[float, float]: + """Find a pair of R1, R2 that satisfies the target.""" + initial = target.initial_test_decades() + search_queue = deque([initial]) + searched_decades = {initial} # tracks everything that has been on the search queue + best = None + + while search_queue: + r1r2_decade = search_queue.popleft() + product = self._generate_e_series_product(self.series, r1r2_decade[0], r1r2_decade[1]) + + for r1, r2 in product: + output = self.value_type.from_resistors( + Range.from_tolerance(r1, self.tolerance), Range.from_tolerance(r2, self.tolerance) + ) + output_dist = output.distance_to(target) + + if best is None or output_dist < best[2]: + best = ((r1, r2), output, output_dist) + if not output_dist: + break + + assert best is not None + if not best[2]: # distance vector empty = satisfying + return best[0] + else: + next_decades = self._get_next_decades(r1r2_decade, target) + for next_decade in next_decades: + if next_decade not in searched_decades and -15 < next_decade[0] < 15 and -15 < next_decade[1] < 15: + searched_decades.add(next_decade) + search_queue.append(next_decade) + + # if it gets here, the search queue has been exhausted without a result + assert best is not None + raise self._no_result_error(best[0], best[1], target) + + def _get_next_decades(self, decade: Tuple[int, int], target: ESeriesRatioValueType) -> List[Tuple[int, int]]: + """If the target was not found scanning the entire decade, this is called to determine next decades to search. + This is passed in the current decade and the target. + + Returns a list of decades to search, in order. Internally the search algorithm deduplicates decades. + This is called for every decade, and results are appended to the end of the search queue after deduplication. + + The default algorithm returns all adjacent combinations of decades where the output + intersects the target. + """ + + def range_of_decade(range_decade: int) -> Range: + """Given a decade, return the range of possible values - for example, decade 0 + would mean 1.0, 2.2, 4.7 and would return a range of (1, 10).""" + return Range(10**range_decade, 10 ** (range_decade + 1)) + + test_decades = [ + # adjustments shifting both decades in the same direction + (decade[0] - 1, decade[1] - 1), + (decade[0] + 1, decade[1] + 1), + # adjustments shifting decades independently + (decade[0] - 1, decade[1]), + (decade[0], decade[1] + 1), + (decade[0] + 1, decade[1]), + (decade[0], decade[1] - 1), + ] + + new_decades = [ + decade + for decade in test_decades + if self.value_type.from_resistors(range_of_decade(decade[0]), range_of_decade(decade[1])).intersects(target) + ] + return new_decades diff --git a/edg/abstract_parts/GateDrivers.py b/edg/abstract_parts/GateDrivers.py index 9b96ff89e..c2f642868 100644 --- a/edg/abstract_parts/GateDrivers.py +++ b/edg/abstract_parts/GateDrivers.py @@ -6,45 +6,48 @@ @abstract_block class HalfBridgeDriver(PowerSwitch, Block): - """Half-bridge driver with independent low / high control for driving two NMOS devices, - with a high-side driver that allows a voltage offset from the main gnd. + """Half-bridge driver with independent low / high control for driving two NMOS devices, + with a high-side driver that allows a voltage offset from the main gnd. - A parameter controls whether a boot diode is required (chip-internal or generated component) or disallowed. - Devices with an internal boot diode must require has_boot_diode=False. - Devices without an internal boot diode may generate an external one. + A parameter controls whether a boot diode is required (chip-internal or generated component) or disallowed. + Devices with an internal boot diode must require has_boot_diode=False. + Devices without an internal boot diode may generate an external one. - This device: - - may or may not have shoot-through protection - - may or may not have an internal bootstrap diode or controller - - may or may not support non-half-bridge topologies (eg, high-side ground required to be the FET common node) + This device: + - may or may not have shoot-through protection + - may or may not have an internal bootstrap diode or controller + - may or may not support non-half-bridge topologies (eg, high-side ground required to be the FET common node) - TODO: auto-generate parameters based on switching frequencies and FET parameters? - """ - def __init__(self, has_boot_diode: BoolLike): - super().__init__() - self.has_boot_diode = self.ArgParameter(has_boot_diode) + TODO: auto-generate parameters based on switching frequencies and FET parameters? + """ - self.pwr = self.Port(VoltageSink.empty(), [Power]) # logic side and low FET - self.gnd = self.Port(Ground.empty(), [Common]) + def __init__(self, has_boot_diode: BoolLike): + super().__init__() + self.has_boot_diode = self.ArgParameter(has_boot_diode) - self.low_out = self.Port(DigitalSource.empty()) # referenced to main gnd + self.pwr = self.Port(VoltageSink.empty(), [Power]) # logic side and low FET + self.gnd = self.Port(Ground.empty(), [Common]) - self.high_pwr = self.Port(VoltageSink.empty(), optional=True) # not used with internal boot diode - self.high_gnd = self.Port(Ground.empty()) # a separate constraint needs to encode voltage limits - self.high_out = self.Port(DigitalSource.empty()) # referenced to high_pwr and high_gnd + self.low_out = self.Port(DigitalSource.empty()) # referenced to main gnd + + self.high_pwr = self.Port(VoltageSink.empty(), optional=True) # not used with internal boot diode + self.high_gnd = self.Port(Ground.empty()) # a separate constraint needs to encode voltage limits + self.high_out = self.Port(DigitalSource.empty()) # referenced to high_pwr and high_gnd class HalfBridgeDriverIndependent(BlockInterfaceMixin[HalfBridgeDriver]): - """Mixin that specifies a half-bridge driver with independent inputs""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.low_in = self.Port(DigitalSink.empty()) - self.high_in = self.Port(DigitalSink.empty()) + """Mixin that specifies a half-bridge driver with independent inputs""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.low_in = self.Port(DigitalSink.empty()) + self.high_in = self.Port(DigitalSink.empty()) class HalfBridgeDriverPwm(BlockInterfaceMixin[HalfBridgeDriver]): - """Mixin that specifies a half-bridge driver with PWM input. - If an enable pin is provided, it should use the optional Resettable mixin""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.pwm_in = self.Port(DigitalSink.empty()) + """Mixin that specifies a half-bridge driver with PWM input. + If an enable pin is provided, it should use the optional Resettable mixin""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.pwm_in = self.Port(DigitalSink.empty()) diff --git a/edg/abstract_parts/GenericCapacitor.py b/edg/abstract_parts/GenericCapacitor.py index 11ee086d7..8e124a5ce 100644 --- a/edg/abstract_parts/GenericCapacitor.py +++ b/edg/abstract_parts/GenericCapacitor.py @@ -10,158 +10,178 @@ class GenericMlcc(Capacitor, SelectorArea, FootprintBlock, GeneratorBlock): - """ - Generic SMT ceramic capacitor (MLCC) picker that chooses a common value (E-series) based on rules - specifying what capacitances / voltage ratings are available in what packages. - - Chosen by a rough scan over available parts on Digikey - at voltages 10v, 16v, 25v, 50v, 100v, 250v - and capacitances 1.0, 2.2, 4.7 - - For Class-1 dielectric (C0G/NP0), 20% tolerance - 0402: 50v/1nF - 0603: 100v/1nF, 50v/2.2nF ? - 0805: 100v/2.2nF, 50v/10nF - 1206: 100v/10nF - - For Class-2 dielectric (X**), 20% tolerance - 0402: 50v / 0.1uF, 25v / 0.1uF, 10v / 2.2uF - 0603: 50v / 0.1uF, 25v / 1uF, 16v / 2.2uF, 10v / 10uF - 0805: 100v / 0.1uF, 50v / 0.1uF (maybe 0.22uF), 25v / 10uF - 1206: 100v / 0.1uF, 50v / 4.7uF, 25v / 10uF, 10v / 22uF - 1210: 100v / 4.7uF, 50v / 10uF, 16v / 22uF, 10v / 47uF - 1812: 100v / 2.2uF, 50v / 1uF, 25v / 10uF (though small sample size) - - Derating coefficients in terms of %capacitance / V over 3.6 - 'Capacitor_SMD:C_0603_1608Metric' # not supported, should not generate below 1uF - """ - SINGLE_CAP_MAX = 22e-6 # maximum capacitance in a single part - MAX_CAP_PACKAGE = 'Capacitor_SMD:C_1206_3216Metric' # default package for largest possible capacitor - - def __init__(self, *args: Any, footprint_spec: StringLike = "", derating_coeff: FloatLike = 1.0, **kwargs: Any) -> None: """ - footprint specifies an optional constraint on footprint - derating_coeff specifies an optional derating coefficient (1.0 = no derating), that does not scale with package. + Generic SMT ceramic capacitor (MLCC) picker that chooses a common value (E-series) based on rules + specifying what capacitances / voltage ratings are available in what packages. + + Chosen by a rough scan over available parts on Digikey + at voltages 10v, 16v, 25v, 50v, 100v, 250v + and capacitances 1.0, 2.2, 4.7 + + For Class-1 dielectric (C0G/NP0), 20% tolerance + 0402: 50v/1nF + 0603: 100v/1nF, 50v/2.2nF ? + 0805: 100v/2.2nF, 50v/10nF + 1206: 100v/10nF + + For Class-2 dielectric (X**), 20% tolerance + 0402: 50v / 0.1uF, 25v / 0.1uF, 10v / 2.2uF + 0603: 50v / 0.1uF, 25v / 1uF, 16v / 2.2uF, 10v / 10uF + 0805: 100v / 0.1uF, 50v / 0.1uF (maybe 0.22uF), 25v / 10uF + 1206: 100v / 0.1uF, 50v / 4.7uF, 25v / 10uF, 10v / 22uF + 1210: 100v / 4.7uF, 50v / 10uF, 16v / 22uF, 10v / 47uF + 1812: 100v / 2.2uF, 50v / 1uF, 25v / 10uF (though small sample size) + + Derating coefficients in terms of %capacitance / V over 3.6 + 'Capacitor_SMD:C_0603_1608Metric' # not supported, should not generate below 1uF """ - super().__init__(*args, **kwargs) - self.footprint_spec = self.ArgParameter(footprint_spec) - self.derating_coeff = self.ArgParameter(derating_coeff) - self.generator_param(self.capacitance, self.voltage, self.footprint_spec, self.footprint_area, self.derating_coeff) - - # Output values - self.selected_nominal_capacitance = self.Parameter(RangeExpr()) - - self.assign(self.actual_capacitance, self.selected_nominal_capacitance) - self.assign(self.actual_voltage_rating, self.voltage) # TODO use package-based voltage rating - - - class SmtCeramicCapacitorGenericPackageSpecs(NamedTuple): - name: str # package name - max: float # maximum nominal capacitance - derate: float # derating coefficient in terms of %capacitance / V over 3.6 - vc_pairs: Dict[float, float] # rough estimate of what the maximum nominal capacitance is at certain voltages - - # package specs in increasing order by size - PACKAGE_SPECS = [ - SmtCeramicCapacitorGenericPackageSpecs( - name='Capacitor_SMD:C_0402_1005Metric', - max=1e-7, - derate=0, - vc_pairs={ 50: 1e-7, 25: 1e-7, 10: 2.2e-6}, - ), - SmtCeramicCapacitorGenericPackageSpecs( - name='Capacitor_SMD:C_0603_1608Metric', - max=1.1e-6, - derate=0, - vc_pairs={ 50: 1e-7, 25: 1e-6, 16: 2.2e-6, 10: 1e-5}, - ), - SmtCeramicCapacitorGenericPackageSpecs( - name='Capacitor_SMD:C_0805_2012Metric', - max=11e-6, - derate=0.08, - vc_pairs={100: 1e-7, 50: 1e-7, 25: 1e-5, }, - ), - SmtCeramicCapacitorGenericPackageSpecs( - name='Capacitor_SMD:C_1206_3216Metric', - max=22e-6, - derate=0.04, - vc_pairs={100: 1e-7, 50: 4.7e-6, 25: 1e-5, 10: 2.2e-5}, - ), - SmtCeramicCapacitorGenericPackageSpecs( - name='Capacitor_SMD:C_1210_3225Metric', - max=4.7e-5, - derate=0, - vc_pairs={100: 4.7e-6, 50: 1e-5, 16: 2.2e-5, 10: 4.7e-5}, - ), - SmtCeramicCapacitorGenericPackageSpecs( - name='Capacitor_SMD:C_1812_4532Metric', - max=1e-4, - derate=0, - vc_pairs={100: 2.2e-6, 50: 1e-6, 25: 1e-5, }, - ), - ] - - @override - def generate(self) -> None: - """ - Selects a generic capacitor without using product tables - - :param capacitance: user-specified (derated) capacitance - :param voltage: user-specified voltage - :param single_nominal_capacitance: used when no single cap with requested capacitance, must generate multiple parallel caps, - actually refers to max capacitance for a given part - :param footprint_spec: user-specified package footprint - :param derating_coeff: user-specified derating coefficient, if used then footprint_spec must be specified - """ - super().generate() - footprint = self.get(self.footprint_spec) - - def select_package(nominal_capacitance: float, voltage: Range) -> Optional[str]: - package_options = [spec for spec in self.PACKAGE_SPECS - if (not footprint or spec.name == footprint) and - (Range.exact(self._footprint_area(spec.name)).fuzzy_in(self.get(self.footprint_area)))] - - for package in package_options: - if package.max >= nominal_capacitance: - for package_max_voltage, package_max_capacitance in package.vc_pairs.items(): - if package_max_voltage >= voltage.upper and package_max_capacitance >= nominal_capacitance: - return package.name - return None - - nominal_capacitance = self.get(self.capacitance) / self.get(self.derating_coeff) - - num_caps = math.ceil(nominal_capacitance.lower / self.SINGLE_CAP_MAX) - if num_caps > 1: - assert num_caps * self.SINGLE_CAP_MAX < nominal_capacitance.upper, "can't generate parallel caps within max capacitance limit" - - self.assign(self.selected_nominal_capacitance, num_caps * nominal_capacitance) - - if footprint == "": - split_package = self.MAX_CAP_PACKAGE - else: - split_package = footprint - - cap_model = DummyCapacitorFootprint( - capacitance=Range.exact(self.SINGLE_CAP_MAX), voltage=self.voltage, - footprint=split_package, - value=f'{UnitUtils.num_to_prefix(self.SINGLE_CAP_MAX, 3)}F') - self.c = ElementDict[DummyCapacitorFootprint]() - for i in range(num_caps): - self.c[i] = self.Block(cap_model) - self.connect(self.c[i].pos, self.pos) - self.connect(self.c[i].neg, self.neg) - else: - value = ESeriesUtil.choose_preferred_number(nominal_capacitance, ESeriesUtil.SERIES[24], 0) - assert value is not None, "cannot select a preferred number" - valid_footprint = select_package(value, self.get(self.voltage)) - assert valid_footprint is not None, "cannot select a valid footprint" - self.assign(self.selected_nominal_capacitance, value) - - self.footprint( - 'C', valid_footprint, - { - '1': self.pos, - '2': self.neg, - }, - value=f'{UnitUtils.num_to_prefix(value, 3)}F' - ) + + SINGLE_CAP_MAX = 22e-6 # maximum capacitance in a single part + MAX_CAP_PACKAGE = "Capacitor_SMD:C_1206_3216Metric" # default package for largest possible capacitor + + def __init__( + self, *args: Any, footprint_spec: StringLike = "", derating_coeff: FloatLike = 1.0, **kwargs: Any + ) -> None: + """ + footprint specifies an optional constraint on footprint + derating_coeff specifies an optional derating coefficient (1.0 = no derating), that does not scale with package. + """ + super().__init__(*args, **kwargs) + self.footprint_spec = self.ArgParameter(footprint_spec) + self.derating_coeff = self.ArgParameter(derating_coeff) + self.generator_param( + self.capacitance, self.voltage, self.footprint_spec, self.footprint_area, self.derating_coeff + ) + + # Output values + self.selected_nominal_capacitance = self.Parameter(RangeExpr()) + + self.assign(self.actual_capacitance, self.selected_nominal_capacitance) + self.assign(self.actual_voltage_rating, self.voltage) # TODO use package-based voltage rating + + class SmtCeramicCapacitorGenericPackageSpecs(NamedTuple): + name: str # package name + max: float # maximum nominal capacitance + derate: float # derating coefficient in terms of %capacitance / V over 3.6 + vc_pairs: Dict[float, float] # rough estimate of what the maximum nominal capacitance is at certain voltages + + # package specs in increasing order by size + PACKAGE_SPECS = [ + SmtCeramicCapacitorGenericPackageSpecs( + name="Capacitor_SMD:C_0402_1005Metric", + max=1e-7, + derate=0, + vc_pairs={50: 1e-7, 25: 1e-7, 10: 2.2e-6}, + ), + SmtCeramicCapacitorGenericPackageSpecs( + name="Capacitor_SMD:C_0603_1608Metric", + max=1.1e-6, + derate=0, + vc_pairs={50: 1e-7, 25: 1e-6, 16: 2.2e-6, 10: 1e-5}, + ), + SmtCeramicCapacitorGenericPackageSpecs( + name="Capacitor_SMD:C_0805_2012Metric", + max=11e-6, + derate=0.08, + vc_pairs={ + 100: 1e-7, + 50: 1e-7, + 25: 1e-5, + }, + ), + SmtCeramicCapacitorGenericPackageSpecs( + name="Capacitor_SMD:C_1206_3216Metric", + max=22e-6, + derate=0.04, + vc_pairs={100: 1e-7, 50: 4.7e-6, 25: 1e-5, 10: 2.2e-5}, + ), + SmtCeramicCapacitorGenericPackageSpecs( + name="Capacitor_SMD:C_1210_3225Metric", + max=4.7e-5, + derate=0, + vc_pairs={100: 4.7e-6, 50: 1e-5, 16: 2.2e-5, 10: 4.7e-5}, + ), + SmtCeramicCapacitorGenericPackageSpecs( + name="Capacitor_SMD:C_1812_4532Metric", + max=1e-4, + derate=0, + vc_pairs={ + 100: 2.2e-6, + 50: 1e-6, + 25: 1e-5, + }, + ), + ] + + @override + def generate(self) -> None: + """ + Selects a generic capacitor without using product tables + + :param capacitance: user-specified (derated) capacitance + :param voltage: user-specified voltage + :param single_nominal_capacitance: used when no single cap with requested capacitance, must generate multiple parallel caps, + actually refers to max capacitance for a given part + :param footprint_spec: user-specified package footprint + :param derating_coeff: user-specified derating coefficient, if used then footprint_spec must be specified + """ + super().generate() + footprint = self.get(self.footprint_spec) + + def select_package(nominal_capacitance: float, voltage: Range) -> Optional[str]: + package_options = [ + spec + for spec in self.PACKAGE_SPECS + if (not footprint or spec.name == footprint) + and (Range.exact(self._footprint_area(spec.name)).fuzzy_in(self.get(self.footprint_area))) + ] + + for package in package_options: + if package.max >= nominal_capacitance: + for package_max_voltage, package_max_capacitance in package.vc_pairs.items(): + if package_max_voltage >= voltage.upper and package_max_capacitance >= nominal_capacitance: + return package.name + return None + + nominal_capacitance = self.get(self.capacitance) / self.get(self.derating_coeff) + + num_caps = math.ceil(nominal_capacitance.lower / self.SINGLE_CAP_MAX) + if num_caps > 1: + assert ( + num_caps * self.SINGLE_CAP_MAX < nominal_capacitance.upper + ), "can't generate parallel caps within max capacitance limit" + + self.assign(self.selected_nominal_capacitance, num_caps * nominal_capacitance) + + if footprint == "": + split_package = self.MAX_CAP_PACKAGE + else: + split_package = footprint + + cap_model = DummyCapacitorFootprint( + capacitance=Range.exact(self.SINGLE_CAP_MAX), + voltage=self.voltage, + footprint=split_package, + value=f"{UnitUtils.num_to_prefix(self.SINGLE_CAP_MAX, 3)}F", + ) + self.c = ElementDict[DummyCapacitorFootprint]() + for i in range(num_caps): + self.c[i] = self.Block(cap_model) + self.connect(self.c[i].pos, self.pos) + self.connect(self.c[i].neg, self.neg) + else: + value = ESeriesUtil.choose_preferred_number(nominal_capacitance, ESeriesUtil.SERIES[24], 0) + assert value is not None, "cannot select a preferred number" + valid_footprint = select_package(value, self.get(self.voltage)) + assert valid_footprint is not None, "cannot select a valid footprint" + self.assign(self.selected_nominal_capacitance, value) + + self.footprint( + "C", + valid_footprint, + { + "1": self.pos, + "2": self.neg, + }, + value=f"{UnitUtils.num_to_prefix(value, 3)}F", + ) diff --git a/edg/abstract_parts/GenericResistor.py b/edg/abstract_parts/GenericResistor.py index e68664bb1..2931723e2 100644 --- a/edg/abstract_parts/GenericResistor.py +++ b/edg/abstract_parts/GenericResistor.py @@ -10,94 +10,106 @@ @non_library class ESeriesResistor(SelectorArea, Resistor, FootprintBlock, GeneratorBlock): - """Default generator that automatically picks resistors from the E-series specified. - Preferentially picks lower E-series (E1 before E3 before E6 ...) value meeting the needs - at the specified tolerance. - Then, picks the minimum (down to 0603, up to 2512) SMD size for the power requirement. - - A series of 0 means exact, but tolerance is still checked. - """ - PACKAGE_POWER: List[Tuple[float, str]] - - def __init__(self, *args: Any, series: IntLike = 24, tolerance: FloatLike = 0.01, - footprint_spec: StringLike = "", **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.series = self.ArgParameter(series) - self.tolerance = self.ArgParameter(tolerance) - self.footprint_spec = self.ArgParameter(footprint_spec) - - self.generator_param(self.resistance, self.power, self.series, self.tolerance, self.footprint_spec, - self.footprint_area) - - @override - def generate(self) -> None: - super().generate() - - resistance = self.get(self.resistance) - tolerance = self.get(self.tolerance) - series = self.get(self.series) - - if series == 0: # exact, not matched to E-series - selected_center = resistance.center() - else: - selected_series = ESeriesUtil.choose_preferred_number(resistance, ESeriesUtil.SERIES[series], tolerance) - if selected_series is None: - raise ValueError("no matching resistor") - selected_center = selected_series - - selected_range = Range.from_tolerance(selected_center, tolerance) - if not selected_range.fuzzy_in(resistance): - raise ValueError(f"chosen resistances tolerance {tolerance} not within {resistance}") - - suitable_packages = [(package_power, package) for package_power, package in self.PACKAGE_POWER - if package_power >= self.get(self.power).upper and - (not self.get(self.footprint_spec) or package == self.get(self.footprint_spec)) and - (Range.exact(self._footprint_area(package)).fuzzy_in(self.get(self.footprint_area)))] - if not suitable_packages: - raise ValueError("no suitable resistor packages") - - self.assign(self.actual_resistance, selected_range) - self.assign(self.actual_power_rating, Range.zero_to_upper(suitable_packages[0][0])) - - self.footprint( - self._standard_footprint().REFDES_PREFIX, suitable_packages[0][1], - self._standard_footprint()._make_pinning(self, suitable_packages[0][1]), - value=f'{UnitUtils.num_to_prefix(selected_center, 3)}, {tolerance * 100:0.3g}%, {suitable_packages[0][0]} W', - ) + """Default generator that automatically picks resistors from the E-series specified. + Preferentially picks lower E-series (E1 before E3 before E6 ...) value meeting the needs + at the specified tolerance. + Then, picks the minimum (down to 0603, up to 2512) SMD size for the power requirement. + + A series of 0 means exact, but tolerance is still checked. + """ + + PACKAGE_POWER: List[Tuple[float, str]] + + def __init__( + self, + *args: Any, + series: IntLike = 24, + tolerance: FloatLike = 0.01, + footprint_spec: StringLike = "", + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self.series = self.ArgParameter(series) + self.tolerance = self.ArgParameter(tolerance) + self.footprint_spec = self.ArgParameter(footprint_spec) + + self.generator_param( + self.resistance, self.power, self.series, self.tolerance, self.footprint_spec, self.footprint_area + ) + + @override + def generate(self) -> None: + super().generate() + + resistance = self.get(self.resistance) + tolerance = self.get(self.tolerance) + series = self.get(self.series) + + if series == 0: # exact, not matched to E-series + selected_center = resistance.center() + else: + selected_series = ESeriesUtil.choose_preferred_number(resistance, ESeriesUtil.SERIES[series], tolerance) + if selected_series is None: + raise ValueError("no matching resistor") + selected_center = selected_series + + selected_range = Range.from_tolerance(selected_center, tolerance) + if not selected_range.fuzzy_in(resistance): + raise ValueError(f"chosen resistances tolerance {tolerance} not within {resistance}") + + suitable_packages = [ + (package_power, package) + for package_power, package in self.PACKAGE_POWER + if package_power >= self.get(self.power).upper + and (not self.get(self.footprint_spec) or package == self.get(self.footprint_spec)) + and (Range.exact(self._footprint_area(package)).fuzzy_in(self.get(self.footprint_area))) + ] + if not suitable_packages: + raise ValueError("no suitable resistor packages") + + self.assign(self.actual_resistance, selected_range) + self.assign(self.actual_power_rating, Range.zero_to_upper(suitable_packages[0][0])) + + self.footprint( + self._standard_footprint().REFDES_PREFIX, + suitable_packages[0][1], + self._standard_footprint()._make_pinning(self, suitable_packages[0][1]), + value=f"{UnitUtils.num_to_prefix(selected_center, 3)}, {tolerance * 100:0.3g}%, {suitable_packages[0][0]} W", + ) class GenericChipResistor(ESeriesResistor): - PACKAGE_POWER = [ # sorted by order of preference (lowest power to highest power) - # picked based on the most common power rating for a size at 100ohm on Digikey - # (1.0/32, '01005'), # KiCad doesn't seem to have a default footprint this small - (1.0/20, 'Resistor_SMD:R_0201_0603Metric'), - (1.0/16, 'Resistor_SMD:R_0402_1005Metric'), - (1.0/10, 'Resistor_SMD:R_0603_1608Metric'), - (1.0/8, 'Resistor_SMD:R_0805_2012Metric'), - (1.0/4, 'Resistor_SMD:R_1206_3216Metric'), - # (1.0/2, 'Resistor_SMD:R_1210_3225Metric'), # actually not that common - # (3.0/4, 'Resistor_SMD:R_2010_5025Metric'), # actually not that common - (1.0, 'Resistor_SMD:R_2512_6332Metric'), # a good portion are also rated for 2W - ] + PACKAGE_POWER = [ # sorted by order of preference (lowest power to highest power) + # picked based on the most common power rating for a size at 100ohm on Digikey + # (1.0/32, '01005'), # KiCad doesn't seem to have a default footprint this small + (1.0 / 20, "Resistor_SMD:R_0201_0603Metric"), + (1.0 / 16, "Resistor_SMD:R_0402_1005Metric"), + (1.0 / 10, "Resistor_SMD:R_0603_1608Metric"), + (1.0 / 8, "Resistor_SMD:R_0805_2012Metric"), + (1.0 / 4, "Resistor_SMD:R_1206_3216Metric"), + # (1.0/2, 'Resistor_SMD:R_1210_3225Metric'), # actually not that common + # (3.0/4, 'Resistor_SMD:R_2010_5025Metric'), # actually not that common + (1.0, "Resistor_SMD:R_2512_6332Metric"), # a good portion are also rated for 2W + ] class GenericAxialResistor(ESeriesResistor): - PACKAGE_POWER = [ # sorted by order of preference (lowest power to highest power) - # picked based on the most common power rating for a size at 100ohm on Digikey - (1.0/8, 'Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P5.08mm_Horizontal'), - (1.0/4, 'Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal'), - (1.0/2, 'Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P12.70mm_Horizontal'), - (1.0, 'Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P12.70mm_Horizontal'), - (2.0, 'Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P15.24mm_Horizontal'), - ] + PACKAGE_POWER = [ # sorted by order of preference (lowest power to highest power) + # picked based on the most common power rating for a size at 100ohm on Digikey + (1.0 / 8, "Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P5.08mm_Horizontal"), + (1.0 / 4, "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal"), + (1.0 / 2, "Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P12.70mm_Horizontal"), + (1.0, "Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P12.70mm_Horizontal"), + (2.0, "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P15.24mm_Horizontal"), + ] class GenericAxialVerticalResistor(ESeriesResistor): - PACKAGE_POWER = [ # sorted by order of preference (lowest power to highest power) - # picked based on the most common power rating for a size at 100ohm on Digikey - (1.0/8, 'Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P1.90mm_Vertical'), - (1.0/4, 'Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical'), - (1.0/2, 'Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P2.54mm_Vertical'), - (1.0, 'Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P5.08mm_Vertical'), - (2.0, 'Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P5.08mm_Vertical'), - ] + PACKAGE_POWER = [ # sorted by order of preference (lowest power to highest power) + # picked based on the most common power rating for a size at 100ohm on Digikey + (1.0 / 8, "Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P1.90mm_Vertical"), + (1.0 / 4, "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical"), + (1.0 / 2, "Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P2.54mm_Vertical"), + (1.0, "Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P5.08mm_Vertical"), + (2.0, "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P5.08mm_Vertical"), + ] diff --git a/edg/abstract_parts/I2cBitBang.py b/edg/abstract_parts/I2cBitBang.py index dedfe7a03..318bcba1d 100644 --- a/edg/abstract_parts/I2cBitBang.py +++ b/edg/abstract_parts/I2cBitBang.py @@ -7,20 +7,21 @@ class I2cControllerBitBang(BitBangAdapter, Block): - """Bit-bang adapter for I2C controller""" - def __init__(self) -> None: - super().__init__() - self.i2c = self.Port(I2cController.empty(), [Output]) - self.scl = self.Port(DigitalBidir.empty()) - self.sda = self.Port(DigitalBidir.empty()) + """Bit-bang adapter for I2C controller""" - @override - def contents(self) -> None: - super().contents() - self.connect(self.i2c.scl, self.scl) - self.connect(self.i2c.sda, self.sda) + def __init__(self) -> None: + super().__init__() + self.i2c = self.Port(I2cController.empty(), [Output]) + self.scl = self.Port(DigitalBidir.empty()) + self.sda = self.Port(DigitalBidir.empty()) - def connected_from(self, scl: Port[DigitalLink], sda: Port[DigitalLink]) -> 'I2cControllerBitBang': - cast(Block, builder.get_enclosing_block()).connect(scl, self.scl) - cast(Block, builder.get_enclosing_block()).connect(sda, self.sda) - return self + @override + def contents(self) -> None: + super().contents() + self.connect(self.i2c.scl, self.scl) + self.connect(self.i2c.sda, self.sda) + + def connected_from(self, scl: Port[DigitalLink], sda: Port[DigitalLink]) -> "I2cControllerBitBang": + cast(Block, builder.get_enclosing_block()).connect(scl, self.scl) + cast(Block, builder.get_enclosing_block()).connect(sda, self.sda) + return self diff --git a/edg/abstract_parts/I2cPullup.py b/edg/abstract_parts/I2cPullup.py index 32f766199..248e36082 100644 --- a/edg/abstract_parts/I2cPullup.py +++ b/edg/abstract_parts/I2cPullup.py @@ -6,18 +6,17 @@ class I2cPullup(Interface, Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - # TODO restrictions on I2C voltage, current draw modeling - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.i2c = self.Port(I2cPullupPort.empty(), [InOut]) + # TODO restrictions on I2C voltage, current draw modeling + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.i2c = self.Port(I2cPullupPort.empty(), [InOut]) + @override + def contents(self) -> None: + super().contents() - @override - def contents(self) -> None: - super().contents() - - res_model = PullupResistor(4.7 * kOhm(tol=0.05)) - self.scl_res = self.Block(res_model).connected(self.pwr, self.i2c.scl) - self.sda_res = self.Block(res_model).connected(self.pwr, self.i2c.sda) + res_model = PullupResistor(4.7 * kOhm(tol=0.05)) + self.scl_res = self.Block(res_model).connected(self.pwr, self.i2c.scl) + self.sda_res = self.Block(res_model).connected(self.pwr, self.i2c.sda) diff --git a/edg/abstract_parts/IdealIoController.py b/edg/abstract_parts/IdealIoController.py index f347b7e31..6d6daff9e 100644 --- a/edg/abstract_parts/IdealIoController.py +++ b/edg/abstract_parts/IdealIoController.py @@ -3,26 +3,55 @@ from ..electronics_model import * from .Categories import IdealModel from .IoController import IoController -from .IoControllerInterfaceMixins import IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerDac, \ - IoControllerCan, IoControllerUsb, IoControllerI2s, IoControllerWifi, IoControllerBluetooth, IoControllerBle +from .IoControllerInterfaceMixins import ( + IoControllerSpiPeripheral, + IoControllerI2cTarget, + IoControllerDac, + IoControllerCan, + IoControllerUsb, + IoControllerI2s, + IoControllerWifi, + IoControllerBluetooth, + IoControllerBle, +) -class IdealIoController(IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerDac, IoControllerCan, - IoControllerUsb, IoControllerI2s, IoControllerWifi, IoControllerBluetooth, IoControllerBle, - IoController, IdealModel, GeneratorBlock): +class IdealIoController( + IoControllerSpiPeripheral, + IoControllerI2cTarget, + IoControllerDac, + IoControllerCan, + IoControllerUsb, + IoControllerI2s, + IoControllerWifi, + IoControllerBluetooth, + IoControllerBle, + IoController, + IdealModel, + GeneratorBlock, +): """An ideal IO controller, with as many IOs as requested. Output have voltages at pwr/gnd, all other parameters are ideal.""" + def __init__(self) -> None: super().__init__() - self.generator_param(self.adc.requested(), self.dac.requested(), self.gpio.requested(), self.spi.requested(), - self.spi_peripheral.requested(), self.i2c.requested(), self.i2c_target.requested(), - self.uart.requested(), self.usb.requested(), self.can.requested(), self.i2s.requested()) + self.generator_param( + self.adc.requested(), + self.dac.requested(), + self.gpio.requested(), + self.spi.requested(), + self.spi_peripheral.requested(), + self.i2c.requested(), + self.i2c_target.requested(), + self.uart.requested(), + self.usb.requested(), + self.can.requested(), + self.i2s.requested(), + ) @override def generate(self) -> None: - self.pwr.init_from(VoltageSink( - current_draw=RangeExpr() - )) + self.pwr.init_from(VoltageSink(current_draw=RangeExpr())) self.gnd.init_from(Ground()) io_current_draw_builder = RangeExpr._to_expr_type(RangeExpr.ZERO) @@ -34,19 +63,18 @@ def generate(self) -> None: for elt in self.get(self.dac.requested()): aout = self.dac.append_elt(AnalogSource.from_supply(self.gnd, self.pwr), elt) io_current_draw_builder = io_current_draw_builder + ( - aout.link().current_drawn.lower().min(0), aout.link().current_drawn.upper().max(0) + aout.link().current_drawn.lower().min(0), + aout.link().current_drawn.upper().max(0), ) - dio_model = DigitalBidir.from_supply( - self.gnd, self.pwr, - pullup_capable=True, pulldown_capable=True - ) + dio_model = DigitalBidir.from_supply(self.gnd, self.pwr, pullup_capable=True, pulldown_capable=True) self.gpio.defined() for elt in self.get(self.gpio.requested()): dio = self.gpio.append_elt(dio_model, elt) io_current_draw_builder = io_current_draw_builder + ( - dio.link().current_drawn.lower().min(0), dio.link().current_drawn.upper().max(0) + dio.link().current_drawn.lower().min(0), + dio.link().current_drawn.upper().max(0), ) self.spi.defined() diff --git a/edg/abstract_parts/IoController.py b/edg/abstract_parts/IoController.py index b4691e030..aecaba0e6 100644 --- a/edg/abstract_parts/IoController.py +++ b/edg/abstract_parts/IoController.py @@ -12,215 +12,237 @@ @non_library @abstract_block class BaseIoController(PinMappable, Block): - """An abstract IO controller block, that takes power input and provides a grab-bag of common IOs. - A base interface for microcontrollers and microcontroller-like devices (eg, FPGAs). - Pin assignments are handled via refinements and can be assigned to pins' allocated names. - - This should not be instantiated as a generic block.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - self.gpio = self.Port(Vector(DigitalBidir.empty()), optional=True, - doc="Microcontroller digital GPIO pins") - self.adc = self.Port(Vector(AnalogSink.empty()), optional=True, - doc="Microcontroller analog input pins") - - self.spi = self.Port(Vector(SpiController.empty()), optional=True, - doc="Microcontroller SPI controllers, each element is an independent SPI controller") - self.i2c = self.Port(Vector(I2cController.empty()), optional=True, - doc="Microcontroller I2C controllers, each element is an independent I2C controller") - self.uart = self.Port(Vector(UartPort.empty()), optional=True, - doc="Microcontroller UARTs") - - # USB should be a mixin, but because it's probably common, it's in base until mixins have GUI support - self.usb = self.Port(Vector(UsbDevicePort.empty()), optional=True, - doc="Microcontroller USB device ports") - - # CAN is now mixins, but automatically materialized for compatibility - # In new code, explicit mixin syntax should be used. - self.can: Vector[CanControllerPort] - from .IoControllerInterfaceMixins import IoControllerCan - self._can_mixin: Optional[IoControllerCan] = None - - self.io_current_draw = self.Parameter(RangeExpr()) # total current draw for all leaf-level IO sinks - - self._io_ports: List[BasePort] = [ # ordered by assignment order, most restrictive should be first - self.adc, self.spi, self.i2c, self.uart, self.usb, self.gpio] - - def __getattr__(self, item: str) -> Any: - # automatically materialize some mixins on abstract classes, only if this is IoController - # note, getattr ONLY called when the field does not exist, and hasattr is implemented via getattr - if self.__class__ is IoController and item == 'can': - if self._can_mixin is None: - from .IoControllerInterfaceMixins import IoControllerCan - self._can_mixin = self.with_mixin(IoControllerCan()) - return self._can_mixin.can - else: - raise AttributeError(item) # ideally we'd use super().__getattr__(...), but that's not defined in base classes - - def _type_of_io(self, io_port: BasePort) -> Type[Port]: - if isinstance(io_port, Vector): - return io_port.elt_type() - elif isinstance(io_port, Port): - return type(io_port) - else: - raise NotImplementedError(f"unknown port type {io_port}") - - @deprecated("use BaseIoControllerExportable") - def _export_ios_from(self, inner: 'BaseIoController', excludes: List[BasePort] = []) -> None: - """Exports all the IO ports from an inner BaseIoController to this block's IO ports. - Optional exclude list, for example if a more complex connection is needed.""" - assert isinstance(inner, BaseIoController), "can only export from inner block of type BaseIoController" - self_ios_by_type = {self._type_of_io(io_port): io_port for io_port in self._io_ports} - exclude_set = IdentitySet(*excludes) - for inner_io in inner._io_ports: - if inner_io in exclude_set: - continue - inner_io_type = self._type_of_io(inner_io) - assert inner_io_type in self_ios_by_type, f"outer missing IO of type {inner_io_type}" - self.connect(self_ios_by_type[inner_io_type], inner_io) - self.assign(self.io_current_draw, inner.io_current_draw) - - @staticmethod - def _instantiate_from(ios: List[BasePort], allocations: List[AllocatedResource]) -> \ - Tuple[Dict[str, CircuitPort], RangeExpr]: - """Given a mapping of port types to IO ports and allocated resources from PinMapUtil, - instantiate vector elements (if a vector) or init the port model (if a port) - for the allocated resources using their data model and return the pin mapping.""" - ios_by_type = {io.elt_type() if isinstance(io, Vector) else type(io): io for io in ios} - pinmap: Dict[str, CircuitPort] = {} - - ports_assigned = IdentitySet[Port]() - io_current_draw_builder = RangeExpr._to_expr_type(RangeExpr.ZERO) - - for io in ios: - if isinstance(io, Vector): - io.defined() # mark all vectors as defined - even if they will be empty - - for allocation in allocations: - io = ios_by_type[type(allocation.port_model)] - - if isinstance(io, Vector): - io_port = io.append_elt(allocation.port_model, allocation.name) - elif isinstance(io, Port): - io_port = io - assert io not in ports_assigned, f"double assignment to {io}" - ports_assigned.add(io) - io.init_from(allocation.port_model) - else: - raise NotImplementedError(f"unknown port type {io}") - - if isinstance(io_port, DigitalBidir): - io_current_draw_builder = io_current_draw_builder + ( - io_port.link().current_drawn.lower().min(0), io_port.link().current_drawn.upper().max(0) + """An abstract IO controller block, that takes power input and provides a grab-bag of common IOs. + A base interface for microcontrollers and microcontroller-like devices (eg, FPGAs). + Pin assignments are handled via refinements and can be assigned to pins' allocated names. + + This should not be instantiated as a generic block.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.gpio = self.Port(Vector(DigitalBidir.empty()), optional=True, doc="Microcontroller digital GPIO pins") + self.adc = self.Port(Vector(AnalogSink.empty()), optional=True, doc="Microcontroller analog input pins") + + self.spi = self.Port( + Vector(SpiController.empty()), + optional=True, + doc="Microcontroller SPI controllers, each element is an independent SPI controller", ) - elif isinstance(io_port, AnalogSink): - pass # assumed no current draw into a sink - elif isinstance(io_port, TouchDriver): - pass # assumed no current draw - elif isinstance(io_port, AnalogSource): - io_current_draw_builder = io_current_draw_builder + ( - io_port.link().current_drawn.lower().min(0), io_port.link().current_drawn.upper().max(0) + self.i2c = self.Port( + Vector(I2cController.empty()), + optional=True, + doc="Microcontroller I2C controllers, each element is an independent I2C controller", ) - elif isinstance(io_port, Bundle): - pass # TODO: don't assume signal bundles have zero current draw - else: - raise NotImplementedError(f"unknown port type {io_port}") - - if isinstance(allocation.pin, str): - assert isinstance(io_port, CircuitPort) - pinmap[allocation.pin] = io_port - elif allocation.pin is None: - assert isinstance(io_port, CircuitPort) # otherwise discarded - elif isinstance(allocation.pin, dict): - assert isinstance(io_port, Bundle) - for (subport_name, (pin_name, pin_resource)) in allocation.pin.items(): - subport = getattr(io_port, subport_name) - assert isinstance(subport, CircuitPort), f"bad sub-port {pin_name} {subport}" - pinmap[pin_name] = subport - else: - raise NotImplementedError(f"unknown allocation pin type {allocation.pin}") - - return (pinmap, io_current_draw_builder) + self.uart = self.Port(Vector(UartPort.empty()), optional=True, doc="Microcontroller UARTs") + # USB should be a mixin, but because it's probably common, it's in base until mixins have GUI support + self.usb = self.Port(Vector(UsbDevicePort.empty()), optional=True, doc="Microcontroller USB device ports") -@non_library -class BaseIoControllerPinmapGenerator(BaseIoController, GeneratorBlock): - """BaseIoController with generator code to set pin mappings""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.pin_assigns) - - @override - def contents(self) -> None: - super().contents() - for io_port in self._io_ports: # defined in contents() so subclass __init__ can define additional _io_ports - if isinstance(io_port, Vector): - self.generator_param(io_port.requested()) - elif isinstance(io_port, Port): - self.generator_param(io_port.is_connected()) - else: - raise NotImplementedError(f"unknown port type {io_port}") - - def _system_pinmap(self) -> Dict[str, CircuitPort]: - """Implement me. Defines the fixed pin mappings from pin number to port.""" - raise NotImplementedError - - def _io_pinmap(self) -> PinMapUtil: - """Implement me. Defines the assignable IO pinmaps.""" - raise NotImplementedError - - def _make_pinning(self) -> Dict[str, CircuitPort]: - allocation_list = [] - for io_port in self._io_ports: - if isinstance(io_port, Vector): # derive Vector connections from requested - allocation_list.append((io_port.elt_type(), self.get(io_port.requested()))) - elif isinstance(io_port, Port): # derive Port connections from is_connected - if self.get(io_port.is_connected()): - requested = [self._name_of_child(io_port, self)] # generate requested name from port name if connected + # CAN is now mixins, but automatically materialized for compatibility + # In new code, explicit mixin syntax should be used. + self.can: Vector[CanControllerPort] + from .IoControllerInterfaceMixins import IoControllerCan + + self._can_mixin: Optional[IoControllerCan] = None + + self.io_current_draw = self.Parameter(RangeExpr()) # total current draw for all leaf-level IO sinks + + self._io_ports: List[BasePort] = [ # ordered by assignment order, most restrictive should be first + self.adc, + self.spi, + self.i2c, + self.uart, + self.usb, + self.gpio, + ] + + def __getattr__(self, item: str) -> Any: + # automatically materialize some mixins on abstract classes, only if this is IoController + # note, getattr ONLY called when the field does not exist, and hasattr is implemented via getattr + if self.__class__ is IoController and item == "can": + if self._can_mixin is None: + from .IoControllerInterfaceMixins import IoControllerCan + + self._can_mixin = self.with_mixin(IoControllerCan()) + return self._can_mixin.can else: - requested = [] - allocation_list.append((type(io_port), requested)) - else: - raise NotImplementedError(f"unknown port type {io_port}") + raise AttributeError( + item + ) # ideally we'd use super().__getattr__(...), but that's not defined in base classes + + def _type_of_io(self, io_port: BasePort) -> Type[Port]: + if isinstance(io_port, Vector): + return io_port.elt_type() + elif isinstance(io_port, Port): + return type(io_port) + else: + raise NotImplementedError(f"unknown port type {io_port}") + + @deprecated("use BaseIoControllerExportable") + def _export_ios_from(self, inner: "BaseIoController", excludes: List[BasePort] = []) -> None: + """Exports all the IO ports from an inner BaseIoController to this block's IO ports. + Optional exclude list, for example if a more complex connection is needed.""" + assert isinstance(inner, BaseIoController), "can only export from inner block of type BaseIoController" + self_ios_by_type = {self._type_of_io(io_port): io_port for io_port in self._io_ports} + exclude_set = IdentitySet(*excludes) + for inner_io in inner._io_ports: + if inner_io in exclude_set: + continue + inner_io_type = self._type_of_io(inner_io) + assert inner_io_type in self_ios_by_type, f"outer missing IO of type {inner_io_type}" + self.connect(self_ios_by_type[inner_io_type], inner_io) + self.assign(self.io_current_draw, inner.io_current_draw) + + @staticmethod + def _instantiate_from( + ios: List[BasePort], allocations: List[AllocatedResource] + ) -> Tuple[Dict[str, CircuitPort], RangeExpr]: + """Given a mapping of port types to IO ports and allocated resources from PinMapUtil, + instantiate vector elements (if a vector) or init the port model (if a port) + for the allocated resources using their data model and return the pin mapping.""" + ios_by_type = {io.elt_type() if isinstance(io, Vector) else type(io): io for io in ios} + pinmap: Dict[str, CircuitPort] = {} + + ports_assigned = IdentitySet[Port]() + io_current_draw_builder = RangeExpr._to_expr_type(RangeExpr.ZERO) + + for io in ios: + if isinstance(io, Vector): + io.defined() # mark all vectors as defined - even if they will be empty + + for allocation in allocations: + io = ios_by_type[type(allocation.port_model)] + + if isinstance(io, Vector): + io_port = io.append_elt(allocation.port_model, allocation.name) + elif isinstance(io, Port): + io_port = io + assert io not in ports_assigned, f"double assignment to {io}" + ports_assigned.add(io) + io.init_from(allocation.port_model) + else: + raise NotImplementedError(f"unknown port type {io}") + + if isinstance(io_port, DigitalBidir): + io_current_draw_builder = io_current_draw_builder + ( + io_port.link().current_drawn.lower().min(0), + io_port.link().current_drawn.upper().max(0), + ) + elif isinstance(io_port, AnalogSink): + pass # assumed no current draw into a sink + elif isinstance(io_port, TouchDriver): + pass # assumed no current draw + elif isinstance(io_port, AnalogSource): + io_current_draw_builder = io_current_draw_builder + ( + io_port.link().current_drawn.lower().min(0), + io_port.link().current_drawn.upper().max(0), + ) + elif isinstance(io_port, Bundle): + pass # TODO: don't assume signal bundles have zero current draw + else: + raise NotImplementedError(f"unknown port type {io_port}") + + if isinstance(allocation.pin, str): + assert isinstance(io_port, CircuitPort) + pinmap[allocation.pin] = io_port + elif allocation.pin is None: + assert isinstance(io_port, CircuitPort) # otherwise discarded + elif isinstance(allocation.pin, dict): + assert isinstance(io_port, Bundle) + for subport_name, (pin_name, pin_resource) in allocation.pin.items(): + subport = getattr(io_port, subport_name) + assert isinstance(subport, CircuitPort), f"bad sub-port {pin_name} {subport}" + pinmap[pin_name] = subport + else: + raise NotImplementedError(f"unknown allocation pin type {allocation.pin}") + + return (pinmap, io_current_draw_builder) - allocated = self._io_pinmap().allocate(allocation_list, self.get(self.pin_assigns)) - self.generator_set_allocation(allocated) - io_pins, current_draw = self._instantiate_from(self._io_ports, allocated) - self.assign(self.io_current_draw, current_draw) - return dict(chain(self._system_pinmap().items(), io_pins.items())) +@non_library +class BaseIoControllerPinmapGenerator(BaseIoController, GeneratorBlock): + """BaseIoController with generator code to set pin mappings""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.pin_assigns) + + @override + def contents(self) -> None: + super().contents() + for io_port in self._io_ports: # defined in contents() so subclass __init__ can define additional _io_ports + if isinstance(io_port, Vector): + self.generator_param(io_port.requested()) + elif isinstance(io_port, Port): + self.generator_param(io_port.is_connected()) + else: + raise NotImplementedError(f"unknown port type {io_port}") + + def _system_pinmap(self) -> Dict[str, CircuitPort]: + """Implement me. Defines the fixed pin mappings from pin number to port.""" + raise NotImplementedError + + def _io_pinmap(self) -> PinMapUtil: + """Implement me. Defines the assignable IO pinmaps.""" + raise NotImplementedError + + def _make_pinning(self) -> Dict[str, CircuitPort]: + allocation_list = [] + for io_port in self._io_ports: + if isinstance(io_port, Vector): # derive Vector connections from requested + allocation_list.append((io_port.elt_type(), self.get(io_port.requested()))) + elif isinstance(io_port, Port): # derive Port connections from is_connected + if self.get(io_port.is_connected()): + requested = [ + self._name_of_child(io_port, self) + ] # generate requested name from port name if connected + else: + requested = [] + allocation_list.append((type(io_port), requested)) + else: + raise NotImplementedError(f"unknown port type {io_port}") + + allocated = self._io_pinmap().allocate(allocation_list, self.get(self.pin_assigns)) + self.generator_set_allocation(allocated) + io_pins, current_draw = self._instantiate_from(self._io_ports, allocated) + self.assign(self.io_current_draw, current_draw) + + return dict(chain(self._system_pinmap().items(), io_pins.items())) def makeIdealIoController() -> Type[Block]: # needed to avoid circular import - from .IdealIoController import IdealIoController - return IdealIoController + from .IdealIoController import IdealIoController + + return IdealIoController @abstract_block_default(makeIdealIoController) class IoController(ProgrammableController, BaseIoController): - """Structural abstract base class for a programmable controller chip (including microcontrollers that take firmware, - and FPGAs that take gateware). + """Structural abstract base class for a programmable controller chip (including microcontrollers that take firmware, + and FPGAs that take gateware). - This provides the model of a grab bag of IOs on its structural interface, and supports common peripherals as - Vectors of GPIO, ADC, I2C, and SPI. The pin_assigns argument can be used to specify how to map Vector elements - to physical (by footprint pin number) or logical pins (by pin name). - Less common peripheral types like CAN and DAC can be added with mixins. + This provides the model of a grab bag of IOs on its structural interface, and supports common peripherals as + Vectors of GPIO, ADC, I2C, and SPI. The pin_assigns argument can be used to specify how to map Vector elements + to physical (by footprint pin number) or logical pins (by pin name). + Less common peripheral types like CAN and DAC can be added with mixins. - This defines a power input port that powers the device, though the IoControllerPowerOut mixin can be used - for a controller that provides power (like USB-powered dev boards). - """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + This defines a power input port that powers the device, though the IoControllerPowerOut mixin can be used + for a controller that provides power (like USB-powered dev boards). + """ - self.gnd = self.Port(Ground.empty(), [Common], optional=True) - self.pwr = self.Port(VoltageSink.empty(), [Power], optional=True) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.gnd = self.Port(Ground.empty(), [Common], optional=True) + self.pwr = self.Port(VoltageSink.empty(), [Power], optional=True) @non_library class IoControllerPowerRequired(IoController): - """IO controller with required power pins.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.require(self.gnd.is_connected()) - self.require(self.pwr.is_connected()) + """IO controller with required power pins.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.require(self.gnd.is_connected()) + self.require(self.pwr.is_connected()) diff --git a/edg/abstract_parts/IoControllerExportable.py b/edg/abstract_parts/IoControllerExportable.py index bc993f1e7..e0a2ebe10 100644 --- a/edg/abstract_parts/IoControllerExportable.py +++ b/edg/abstract_parts/IoControllerExportable.py @@ -14,6 +14,7 @@ class BaseIoControllerExportable(BaseIoController, GeneratorBlock): The export is also customizable, e.g. if additional subcircuits are needed for some connection. Also defines a function for adding additional internal pin assignments. The internal device (self.ic) must have been created (e.g., in contents()) before this generate() is called.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.ic: BaseIoController @@ -30,9 +31,11 @@ def contents(self) -> None: # TODO can this be deduplicated w/ BaseIoController else: raise NotImplementedError(f"unknown port type {io_port}") - ExportType = TypeVar('ExportType', bound=Port) - def _make_export_vector(self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, - assign: Optional[str]) -> Optional[str]: + ExportType = TypeVar("ExportType", bound=Port) + + def _make_export_vector( + self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, assign: Optional[str] + ) -> Optional[str]: """Connects my external IO to some inner IO, for a requested port in my array-typed IO. This function can be overloaded to handle special cases, e.g. if additional circuitry is required. Called within generate, has access to generator params. @@ -59,7 +62,7 @@ def generate(self) -> None: # mutated in-place during _make_export_* assigns_raw = self.get(self.pin_assigns).copy() assigns = cast(List[Optional[str]], assigns_raw) - assign_index_by_name = {assign.split('=')[0]: i for i, assign in enumerate(assigns_raw)} + assign_index_by_name = {assign.split("=")[0]: i for i, assign in enumerate(assigns_raw)} for self_io in self._io_ports: self_io_type = self._type_of_io(self_io) diff --git a/edg/abstract_parts/IoControllerInterfaceMixins.py b/edg/abstract_parts/IoControllerInterfaceMixins.py index abee9bcf3..22b25e2ac 100644 --- a/edg/abstract_parts/IoControllerInterfaceMixins.py +++ b/edg/abstract_parts/IoControllerInterfaceMixins.py @@ -8,8 +8,11 @@ class IoControllerSpiPeripheral(BlockInterfaceMixin[BaseIoController]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.spi_peripheral = self.Port(Vector(SpiPeripheral.empty()), optional=True, - doc="Microcontroller SPI peripherals (excluding CS pin, which must be handled separately), each element is an independent SPI peripheral") + self.spi_peripheral = self.Port( + Vector(SpiPeripheral.empty()), + optional=True, + doc="Microcontroller SPI peripherals (excluding CS pin, which must be handled separately), each element is an independent SPI peripheral", + ) self.implementation(lambda base: base._io_ports.append(self.spi_peripheral)) @@ -17,8 +20,11 @@ class IoControllerI2cTarget(BlockInterfaceMixin[BaseIoController]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.i2c_target = self.Port(Vector(I2cTarget.empty()), optional=True, - doc="Microcontroller I2C targets, each element is an independent I2C target") + self.i2c_target = self.Port( + Vector(I2cTarget.empty()), + optional=True, + doc="Microcontroller I2C targets, each element is an independent I2C target", + ) self.implementation(lambda base: base._io_ports.append(self.i2c_target)) @@ -26,8 +32,7 @@ class IoControllerTouchDriver(BlockInterfaceMixin[BaseIoController]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.touch = self.Port(Vector(TouchDriver.empty()), optional=True, - doc="Microcontroller touch input") + self.touch = self.Port(Vector(TouchDriver.empty()), optional=True, doc="Microcontroller touch input") self.implementation(lambda base: base._io_ports.insert(0, self.touch)) # allocate first @@ -35,8 +40,7 @@ class IoControllerDac(BlockInterfaceMixin[BaseIoController]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.dac = self.Port(Vector(AnalogSource.empty()), optional=True, - doc="Microcontroller analog output pins") + self.dac = self.Port(Vector(AnalogSource.empty()), optional=True, doc="Microcontroller analog output pins") self.implementation(lambda base: base._io_ports.insert(0, self.dac)) # allocate first @@ -44,8 +48,9 @@ class IoControllerCan(BlockInterfaceMixin[BaseIoController]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.can = self.Port(Vector(CanControllerPort.empty()), optional=True, - doc="Microcontroller CAN controller ports") + self.can = self.Port( + Vector(CanControllerPort.empty()), optional=True, doc="Microcontroller CAN controller ports" + ) self.implementation(lambda base: base._io_ports.append(self.can)) @@ -55,14 +60,15 @@ class IoControllerUsb(BlockInterfaceMixin[BaseIoController]): This class SHOULD BE mixed into IoController blocks, in preparation for the eventual move. This WILL NOT WORK when used in .with_mixin, since this defines no fields.""" + pass + class IoControllerUsbCc(BlockInterfaceMixin[BaseIoController]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.cc = self.Port(Vector(UsbCcPort.empty()), optional=True, - doc="Microcontroller USB Power delivery CC pins") + self.cc = self.Port(Vector(UsbCcPort.empty()), optional=True, doc="Microcontroller USB Power delivery CC pins") self.implementation(lambda base: base._io_ports.append(self.cc)) @@ -70,8 +76,11 @@ class IoControllerI2s(BlockInterfaceMixin[BaseIoController]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.i2s = self.Port(Vector(I2sController.empty()), optional=True, - doc="Microcontroller I2S controller ports, each element is an independent I2S controller") + self.i2s = self.Port( + Vector(I2sController.empty()), + optional=True, + doc="Microcontroller I2S controller ports, each element is an independent I2S controller", + ) self.implementation(lambda base: base._io_ports.append(self.i2s)) @@ -79,8 +88,9 @@ class IoControllerDvp8(BlockInterfaceMixin[BaseIoController]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.dvp8 = self.Port(Vector(Dvp8Host.empty()), optional=True, - doc="Microcontroller 8-bit DVP digital video ports") + self.dvp8 = self.Port( + Vector(Dvp8Host.empty()), optional=True, doc="Microcontroller 8-bit DVP digital video ports" + ) self.implementation(lambda base: base._io_ports.append(self.dvp8)) @@ -98,23 +108,31 @@ class IoControllerBle(BlockInterfaceMixin[BaseIoController]): class IoControllerPowerOut(BlockInterfaceMixin[IoController]): """IO controller mixin that provides an output of the IO controller's VddIO rail, commonly 3.3v.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.pwr_out = self.Port(VoltageSource.empty(), optional=True, - doc="Power output port, typically of the device's Vdd or VddIO rail at 3.3v") + self.pwr_out = self.Port( + VoltageSource.empty(), + optional=True, + doc="Power output port, typically of the device's Vdd or VddIO rail at 3.3v", + ) class IoControllerUsbOut(BlockInterfaceMixin[IoController]): """IO controller mixin that provides an output of the IO controller's USB Vbus.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.vusb_out = self.Port(VoltageSource.empty(), optional=True, - doc="Power output port of the device's Vbus, typically 5v") + self.vusb_out = self.Port( + VoltageSource.empty(), optional=True, doc="Power output port of the device's Vbus, typically 5v" + ) class IoControllerVin(BlockInterfaceMixin[IoController]): """IO controller mixin that provides a >=5v input to the device, typically upstream of the Vbus-to-3.3 regulator.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.pwr_vin = self.Port(VoltageSink.empty(), optional=True, - doc="Power input pin, typically rated for 5v or a bit beyond.") + self.pwr_vin = self.Port( + VoltageSink.empty(), optional=True, doc="Power input pin, typically rated for 5v or a bit beyond." + ) diff --git a/edg/abstract_parts/IoControllerMixins.py b/edg/abstract_parts/IoControllerMixins.py index 3aa684e16..fc7b52cb3 100644 --- a/edg/abstract_parts/IoControllerMixins.py +++ b/edg/abstract_parts/IoControllerMixins.py @@ -7,22 +7,23 @@ @non_library class WithCrystalGenerator(IoController, GeneratorBlock): - """A Block generator mixin that checks if a crystal oscillator is needed, and if so generates it.""" - DEFAULT_CRYSTAL_FREQUENCY: RangeLike + """A Block generator mixin that checks if a crystal oscillator is needed, and if so generates it.""" - def __init__(self) -> None: - super().__init__() - self.xtal_node = self.connect() # connect this internal node to the microcontroller; this may be empty + DEFAULT_CRYSTAL_FREQUENCY: RangeLike - def _crystal_required(self) -> bool: - """Integration point to determine whether a crystal is required. - Called within generate, has access to generator params.""" - return False + def __init__(self) -> None: + super().__init__() + self.xtal_node = self.connect() # connect this internal node to the microcontroller; this may be empty - @override - def generate(self) -> None: - super().generate() - if self._crystal_required(): - self.crystal = self.Block(OscillatorReference(self.DEFAULT_CRYSTAL_FREQUENCY)) - self.connect(self.crystal.gnd, self.gnd) - self.connect(self.xtal_node, self.crystal.crystal) + def _crystal_required(self) -> bool: + """Integration point to determine whether a crystal is required. + Called within generate, has access to generator params.""" + return False + + @override + def generate(self) -> None: + super().generate() + if self._crystal_required(): + self.crystal = self.Block(OscillatorReference(self.DEFAULT_CRYSTAL_FREQUENCY)) + self.connect(self.crystal.gnd, self.gnd) + self.connect(self.xtal_node, self.crystal.crystal) diff --git a/edg/abstract_parts/IoControllerProgramming.py b/edg/abstract_parts/IoControllerProgramming.py index f27633d2f..473cd2028 100644 --- a/edg/abstract_parts/IoControllerProgramming.py +++ b/edg/abstract_parts/IoControllerProgramming.py @@ -3,52 +3,59 @@ from typing_extensions import override from ..electronics_model import * -from .AbstractDebugHeaders import SwdCortexTargetConnector, SwdCortexTargetConnectorReset, SwdCortexTargetConnectorSwo, \ - SwdCortexTargetConnectorTdi +from .AbstractDebugHeaders import ( + SwdCortexTargetConnector, + SwdCortexTargetConnectorReset, + SwdCortexTargetConnectorSwo, + SwdCortexTargetConnectorTdi, +) from .IoController import IoController from .IoControllerExportable import BaseIoControllerExportable @non_library class IoControllerWithSwdTargetConnector(IoController, BaseIoControllerExportable): - """An IoController with a SWD programming header and optional SWO and TDI pins that - can be assigned to any microcontroller pin. - - This defines the interface for the SWO and TDI pin spec (passed to the pin assignment), - and instantiates a SWD target with connected power and ground. SWD must be connected by - the subclass.""" - def __init__(self, swd_swo_pin: StringLike = "NC", swd_tdi_pin: StringLike = "NC", swd_connect_reset: BoolLike = True): - super().__init__() - self.swd_swo_pin = self.ArgParameter(swd_swo_pin) - self.swd_tdi_pin = self.ArgParameter(swd_tdi_pin) - self.swd_connect_reset = self.ArgParameter(swd_connect_reset) - self.generator_param(self.swd_swo_pin, self.swd_tdi_pin, self.swd_connect_reset) - self.swd_node = self.connect() # connect this internal node to the microcontroller - self.reset_node = self.connect() # connect this internal node to the microcontroller - - @override - def contents(self) -> None: - super().contents() - self.swd = self.Block(SwdCortexTargetConnector()) - self.connect(self.swd.gnd, self.gnd) - self.connect(self.swd.pwr, self.pwr) - self.connect(self.swd_node, self.swd.swd) - - @override - def _inner_pin_assigns(self, assigns: List[str]) -> List[str]: - assigns = super()._inner_pin_assigns(assigns) - if self.get(self.swd_swo_pin) != 'NC': - assigns.append(f'swd_swo={self.get(self.swd_swo_pin)}') - if self.get(self.swd_tdi_pin) != 'NC': - assigns.append(f'swd_tdi={self.get(self.swd_tdi_pin)}') - return assigns - - @override - def generate(self) -> None: - super().generate() - if self.get(self.swd_swo_pin) != 'NC': - self.connect(self.ic.gpio.request('swd_swo'), self.swd.with_mixin(SwdCortexTargetConnectorSwo()).swo) - if self.get(self.swd_tdi_pin) != 'NC': - self.connect(self.ic.gpio.request('swd_tdi'), self.swd.with_mixin(SwdCortexTargetConnectorTdi()).tdi) - if self.get(self.swd_connect_reset): # reset commonly connected but not required by SWD - self.connect(self.reset_node, self.swd.with_mixin(SwdCortexTargetConnectorReset()).reset) + """An IoController with a SWD programming header and optional SWO and TDI pins that + can be assigned to any microcontroller pin. + + This defines the interface for the SWO and TDI pin spec (passed to the pin assignment), + and instantiates a SWD target with connected power and ground. SWD must be connected by + the subclass.""" + + def __init__( + self, swd_swo_pin: StringLike = "NC", swd_tdi_pin: StringLike = "NC", swd_connect_reset: BoolLike = True + ): + super().__init__() + self.swd_swo_pin = self.ArgParameter(swd_swo_pin) + self.swd_tdi_pin = self.ArgParameter(swd_tdi_pin) + self.swd_connect_reset = self.ArgParameter(swd_connect_reset) + self.generator_param(self.swd_swo_pin, self.swd_tdi_pin, self.swd_connect_reset) + self.swd_node = self.connect() # connect this internal node to the microcontroller + self.reset_node = self.connect() # connect this internal node to the microcontroller + + @override + def contents(self) -> None: + super().contents() + self.swd = self.Block(SwdCortexTargetConnector()) + self.connect(self.swd.gnd, self.gnd) + self.connect(self.swd.pwr, self.pwr) + self.connect(self.swd_node, self.swd.swd) + + @override + def _inner_pin_assigns(self, assigns: List[str]) -> List[str]: + assigns = super()._inner_pin_assigns(assigns) + if self.get(self.swd_swo_pin) != "NC": + assigns.append(f"swd_swo={self.get(self.swd_swo_pin)}") + if self.get(self.swd_tdi_pin) != "NC": + assigns.append(f"swd_tdi={self.get(self.swd_tdi_pin)}") + return assigns + + @override + def generate(self) -> None: + super().generate() + if self.get(self.swd_swo_pin) != "NC": + self.connect(self.ic.gpio.request("swd_swo"), self.swd.with_mixin(SwdCortexTargetConnectorSwo()).swo) + if self.get(self.swd_tdi_pin) != "NC": + self.connect(self.ic.gpio.request("swd_tdi"), self.swd.with_mixin(SwdCortexTargetConnectorTdi()).tdi) + if self.get(self.swd_connect_reset): # reset commonly connected but not required by SWD + self.connect(self.reset_node, self.swd.with_mixin(SwdCortexTargetConnectorReset()).reset) diff --git a/edg/abstract_parts/LevelShifter.py b/edg/abstract_parts/LevelShifter.py index 164f5fe11..a69af74bb 100644 --- a/edg/abstract_parts/LevelShifter.py +++ b/edg/abstract_parts/LevelShifter.py @@ -6,6 +6,7 @@ from .AbstractFets import Fet from .DummyDevices import DummyVoltageSink + class BidirectionaLevelShifter(Interface, GeneratorBlock): """Bidirectional level shifter for low(ish) frequency signals. Circuit design from Phillips AN97055, https://cdn-shop.adafruit.com/datasheets/an97055.pdf @@ -21,8 +22,13 @@ class BidirectionaLevelShifter(Interface, GeneratorBlock): If empty, both sides are assumed to be able to drive the shifter and must have voltages and output thresholds modeled. TODO: this mode may be brittle """ - def __init__(self, lv_res: RangeLike = 4.7*kOhm(tol=0.05), hv_res: RangeLike = 4.7*kOhm(tol=0.05), - src_hint: StringLike = '') -> None: + + def __init__( + self, + lv_res: RangeLike = 4.7 * kOhm(tol=0.05), + hv_res: RangeLike = 4.7 * kOhm(tol=0.05), + src_hint: StringLike = "", + ) -> None: super().__init__() self.lv_pwr = self.Port(VoltageSink.empty()) self.lv_io = self.Port(DigitalBidir.empty()) @@ -38,33 +44,35 @@ def __init__(self, lv_res: RangeLike = 4.7*kOhm(tol=0.05), hv_res: RangeLike = 4 def generate(self) -> None: super().generate() - self.fet = self.Block(Fet.NFet( - drain_voltage=self.hv_pwr.link().voltage.hull(self.hv_io.link().voltage), - drain_current=self.lv_io.link().current_drawn.hull(self.hv_io.link().current_drawn), - gate_voltage=self.lv_pwr.link().voltage - self.lv_io.link().voltage, - rds_on=(0, 1)*Ohm # arbitrary - )) + self.fet = self.Block( + Fet.NFet( + drain_voltage=self.hv_pwr.link().voltage.hull(self.hv_io.link().voltage), + drain_current=self.lv_io.link().current_drawn.hull(self.hv_io.link().current_drawn), + gate_voltage=self.lv_pwr.link().voltage - self.lv_io.link().voltage, + rds_on=(0, 1) * Ohm, # arbitrary + ) + ) - if self.get(self.src_hint) == 'lv': # LV is source, HV model is incomplete + if self.get(self.src_hint) == "lv": # LV is source, HV model is incomplete lv_io_model = DigitalBidir( voltage_out=self.lv_pwr.link().voltage, # this is not driving, effectively only a pullup - output_thresholds=self.lv_pwr.link().voltage.hull(-float('inf')) + output_thresholds=self.lv_pwr.link().voltage.hull(-float("inf")), ) else: # HV model is complete, can use its thresholds lv_io_model = DigitalBidir( voltage_out=self.lv_pwr.link().voltage.hull(self.hv_io.link().voltage.lower()), - output_thresholds=self.lv_pwr.link().voltage.hull(self.hv_io.link().voltage.lower()) + output_thresholds=self.lv_pwr.link().voltage.hull(self.hv_io.link().voltage.lower()), ) - if self.get(self.src_hint) == 'hv': # HV is source, LV model is incomplete + if self.get(self.src_hint) == "hv": # HV is source, LV model is incomplete hv_io_model = DigitalBidir( voltage_out=self.hv_pwr.link().voltage, # this is not driving, effectively only a pullup - output_thresholds=self.hv_pwr.link().voltage.hull(-float('inf')) + output_thresholds=self.hv_pwr.link().voltage.hull(-float("inf")), ) else: # HV model is complete, can use its thresholds hv_io_model = DigitalBidir( voltage_out=self.hv_pwr.link().voltage.hull(self.lv_io.link().voltage.lower()), - output_thresholds=self.hv_pwr.link().voltage.hull(self.lv_io.link().voltage.lower()) + output_thresholds=self.hv_pwr.link().voltage.hull(self.lv_io.link().voltage.lower()), ) self.connect(self.lv_io, self.fet.source.adapt_to(lv_io_model)) diff --git a/edg/abstract_parts/MergedBlocks.py b/edg/abstract_parts/MergedBlocks.py index 1364d02f4..9adddd40a 100644 --- a/edg/abstract_parts/MergedBlocks.py +++ b/edg/abstract_parts/MergedBlocks.py @@ -7,136 +7,132 @@ class MergedVoltageSource(DummyDevice, NetBlock, GeneratorBlock): - def __init__(self) -> None: - super().__init__() - - self.pwr_ins = self.Port(Vector(VoltageSink.empty())) - self.pwr_out = self.Port(VoltageSource( - voltage_out=RangeExpr(), - current_limits=RangeExpr.ALL - )) - self.generator_param(self.pwr_ins.requested()) - - @override - def generate(self) -> None: - super().generate() - self.pwr_ins.defined() - for in_request in self.get(self.pwr_ins.requested()): - self.pwr_ins.append_elt(VoltageSink( - voltage_limits=RangeExpr.ALL, - current_draw=self.pwr_out.link().current_drawn - ), in_request) - - self.assign(self.pwr_out.voltage_out, - self.pwr_ins.hull(lambda x: x.link().voltage)) - - def connected_from(self, *pwr_ins: Port[VoltageLink]) -> 'MergedVoltageSource': - for pwr_in in pwr_ins: - cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_ins.request()) - return self + def __init__(self) -> None: + super().__init__() + + self.pwr_ins = self.Port(Vector(VoltageSink.empty())) + self.pwr_out = self.Port(VoltageSource(voltage_out=RangeExpr(), current_limits=RangeExpr.ALL)) + self.generator_param(self.pwr_ins.requested()) + + @override + def generate(self) -> None: + super().generate() + self.pwr_ins.defined() + for in_request in self.get(self.pwr_ins.requested()): + self.pwr_ins.append_elt( + VoltageSink(voltage_limits=RangeExpr.ALL, current_draw=self.pwr_out.link().current_drawn), in_request + ) + + self.assign(self.pwr_out.voltage_out, self.pwr_ins.hull(lambda x: x.link().voltage)) + + def connected_from(self, *pwr_ins: Port[VoltageLink]) -> "MergedVoltageSource": + for pwr_in in pwr_ins: + cast(Block, builder.get_enclosing_block()).connect(pwr_in, self.pwr_ins.request()) + return self class MergedDigitalSource(DummyDevice, NetBlock, GeneratorBlock): - def __init__(self) -> None: - super().__init__() - - self.ins = self.Port(Vector(DigitalSink.empty())) - self.out = self.Port(DigitalSource( - voltage_out=RangeExpr(), - output_thresholds=RangeExpr(), - pullup_capable=BoolExpr(), pulldown_capable=BoolExpr() - )) - self.generator_param(self.ins.requested()) - - @override - def generate(self) -> None: - super().generate() - self.ins.defined() - for in_request in self.get(self.ins.requested()): - self.ins.append_elt(DigitalSink( - current_draw=self.out.link().current_drawn, - ), in_request) - - self.assign(self.out.voltage_out, - self.ins.hull(lambda x: x.link().voltage)) - self.assign(self.out.output_thresholds, - self.ins.intersection(lambda x: x.link().output_thresholds)) - self.assign(self.out.pullup_capable, - self.ins.any(lambda x: x.link().pullup_capable)) - self.assign(self.out.pulldown_capable, - self.ins.any(lambda x: x.link().pulldown_capable)) - - def connected_from(self, *ins: Port[DigitalLink]) -> 'MergedDigitalSource': - for in_port in ins: - cast(Block, builder.get_enclosing_block()).connect(in_port, self.ins.request()) - return self + def __init__(self) -> None: + super().__init__() + + self.ins = self.Port(Vector(DigitalSink.empty())) + self.out = self.Port( + DigitalSource( + voltage_out=RangeExpr(), + output_thresholds=RangeExpr(), + pullup_capable=BoolExpr(), + pulldown_capable=BoolExpr(), + ) + ) + self.generator_param(self.ins.requested()) + + @override + def generate(self) -> None: + super().generate() + self.ins.defined() + for in_request in self.get(self.ins.requested()): + self.ins.append_elt( + DigitalSink( + current_draw=self.out.link().current_drawn, + ), + in_request, + ) + + self.assign(self.out.voltage_out, self.ins.hull(lambda x: x.link().voltage)) + self.assign(self.out.output_thresholds, self.ins.intersection(lambda x: x.link().output_thresholds)) + self.assign(self.out.pullup_capable, self.ins.any(lambda x: x.link().pullup_capable)) + self.assign(self.out.pulldown_capable, self.ins.any(lambda x: x.link().pulldown_capable)) + + def connected_from(self, *ins: Port[DigitalLink]) -> "MergedDigitalSource": + for in_port in ins: + cast(Block, builder.get_enclosing_block()).connect(in_port, self.ins.request()) + return self class MergedAnalogSource(KiCadImportableBlock, DummyDevice, NetBlock, GeneratorBlock): - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name.startswith('edg_importable:Merge') # can be any merge - count = int(symbol_name.removeprefix('edg_importable:Merge')) - pins: Dict[str, BasePort] = {'0': self.output} - pins.update({str(i+1): self.inputs.request() for i in range(count)}) - return pins - - def __init__(self) -> None: - super().__init__() - self.output = self.Port(AnalogSource( - voltage_out=RangeExpr(), - signal_out=RangeExpr(), - impedance=RangeExpr() - )) - self.inputs = self.Port(Vector(AnalogSink.empty())) - self.generator_param(self.inputs.requested()) - - @override - def generate(self) -> None: - super().generate() - self.inputs.defined() - for in_request in self.get(self.inputs.requested()): - self.inputs.append_elt(AnalogSink( - current_draw=self.output.link().current_drawn, - impedance=self.output.link().sink_impedance - ), in_request) - - self.assign(self.output.voltage_out, self.inputs.hull(lambda x: x.link().voltage)) - self.assign(self.output.signal_out, self.inputs.hull(lambda x: x.link().signal)) - self.assign(self.output.impedance, # covering cases of any or all sources driving - self.inputs.hull(lambda x: x.link().source_impedance).hull( - 1 / (1 / self.inputs.map_extract(lambda x: x.link().source_impedance)).sum())) - - def connected_from(self, *inputs: Port[AnalogLink]) -> 'MergedAnalogSource': - for input in inputs: - cast(Block, builder.get_enclosing_block()).connect(input, self.inputs.request()) - return self + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name.startswith("edg_importable:Merge") # can be any merge + count = int(symbol_name.removeprefix("edg_importable:Merge")) + pins: Dict[str, BasePort] = {"0": self.output} + pins.update({str(i + 1): self.inputs.request() for i in range(count)}) + return pins + + def __init__(self) -> None: + super().__init__() + self.output = self.Port(AnalogSource(voltage_out=RangeExpr(), signal_out=RangeExpr(), impedance=RangeExpr())) + self.inputs = self.Port(Vector(AnalogSink.empty())) + self.generator_param(self.inputs.requested()) + + @override + def generate(self) -> None: + super().generate() + self.inputs.defined() + for in_request in self.get(self.inputs.requested()): + self.inputs.append_elt( + AnalogSink(current_draw=self.output.link().current_drawn, impedance=self.output.link().sink_impedance), + in_request, + ) + + self.assign(self.output.voltage_out, self.inputs.hull(lambda x: x.link().voltage)) + self.assign(self.output.signal_out, self.inputs.hull(lambda x: x.link().signal)) + self.assign( + self.output.impedance, # covering cases of any or all sources driving + self.inputs.hull(lambda x: x.link().source_impedance).hull( + 1 / (1 / self.inputs.map_extract(lambda x: x.link().source_impedance)).sum() + ), + ) + + def connected_from(self, *inputs: Port[AnalogLink]) -> "MergedAnalogSource": + for input in inputs: + cast(Block, builder.get_enclosing_block()).connect(input, self.inputs.request()) + return self class MergedSpiController(DummyDevice, GeneratorBlock): - def __init__(self) -> None: - super().__init__() - self.ins = self.Port(Vector(SpiPeripheral.empty())) - self.out = self.Port(SpiController.empty()) - self.generator_param(self.ins.requested()) - - @override - def generate(self) -> None: - super().generate() - self.sck_merge = self.Block(MergedDigitalSource()) - self.connect(self.sck_merge.out, self.out.sck) - self.mosi_merge = self.Block(MergedDigitalSource()) - self.connect(self.mosi_merge.out, self.out.mosi) - miso_net = self.connect(self.out.miso) # can be directly connected - - self.ins.defined() - for in_request in self.get(self.ins.requested()): - in_port = self.ins.append_elt(SpiPeripheral.empty(), in_request) - self.connect(miso_net, in_port.miso) - self.connect(self.sck_merge.ins.request(in_request), in_port.sck) - self.connect(self.mosi_merge.ins.request(in_request), in_port.mosi) - - def connected_from(self, *ins: Port[SpiLink]) -> 'MergedSpiController': - for in_port in ins: - cast(Block, builder.get_enclosing_block()).connect(in_port, self.ins.request()) - return self + def __init__(self) -> None: + super().__init__() + self.ins = self.Port(Vector(SpiPeripheral.empty())) + self.out = self.Port(SpiController.empty()) + self.generator_param(self.ins.requested()) + + @override + def generate(self) -> None: + super().generate() + self.sck_merge = self.Block(MergedDigitalSource()) + self.connect(self.sck_merge.out, self.out.sck) + self.mosi_merge = self.Block(MergedDigitalSource()) + self.connect(self.mosi_merge.out, self.out.mosi) + miso_net = self.connect(self.out.miso) # can be directly connected + + self.ins.defined() + for in_request in self.get(self.ins.requested()): + in_port = self.ins.append_elt(SpiPeripheral.empty(), in_request) + self.connect(miso_net, in_port.miso) + self.connect(self.sck_merge.ins.request(in_request), in_port.sck) + self.connect(self.mosi_merge.ins.request(in_request), in_port.mosi) + + def connected_from(self, *ins: Port[SpiLink]) -> "MergedSpiController": + for in_port in ins: + cast(Block, builder.get_enclosing_block()).connect(in_port, self.ins.request()) + return self diff --git a/edg/abstract_parts/Nonstrict3v3Compatible.py b/edg/abstract_parts/Nonstrict3v3Compatible.py index d6f2f3798..bcbca076c 100644 --- a/edg/abstract_parts/Nonstrict3v3Compatible.py +++ b/edg/abstract_parts/Nonstrict3v3Compatible.py @@ -8,6 +8,7 @@ class Nonstrict3v3Compatible(BlockInterfaceMixin[Block]): within the absolute maximum, setting the nonstrict_3v3_compatible parameter to True extends the modeled voltage range to 3.6v or the absolute maximum, whichever is lower. Occurs in displays.""" + def __init__(self, *args: Any, nonstrict_3v3_compatible: BoolLike = False, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.nonstrict_3v3_compatible = self.ArgParameter(nonstrict_3v3_compatible) diff --git a/edg/abstract_parts/OpampCircuits.py b/edg/abstract_parts/OpampCircuits.py index 938e5955e..2b0a7a4d9 100644 --- a/edg/abstract_parts/OpampCircuits.py +++ b/edg/abstract_parts/OpampCircuits.py @@ -14,410 +14,440 @@ class OpampFollower(OpampApplication, KiCadSchematicBlock, KiCadImportableBlock): - """Opamp follower circuit, outputs the same signal as the input (but probably stronger).""" - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name == 'edg_importable:Follower' - return { - '1': self.input, '3': self.output, 'V+': self.pwr, 'V-': self.gnd - } - - def __init__(self) -> None: - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input = self.Port(AnalogSink.empty(), [Input]) - self.output = self.Port(AnalogSource.empty(), [Output]) - - @override - def contents(self) -> None: - super().contents() - - self.amp = self.Block(Opamp()) - self.forced = self.Block(ForcedAnalogSignal(self.input.link().signal)) - - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch")) - - -class AmplifierValues(ESeriesRatioValue['AmplifierValues']): - def __init__(self, amplification: Range, parallel_impedance: Range): - self.amplification = amplification # amplification factor from in to out - self.parallel_impedance = parallel_impedance # parallel impedance into the opamp negative pin - - @staticmethod - @override - def from_resistors(r1_range: Range, r2_range: Range) -> 'AmplifierValues': - """r2 is the low-side resistor (Vin- to GND) and r1 is the high-side resistor (Vin- to Vout).""" - return AmplifierValues( - (r1_range / r2_range) + 1, - 1 / (1 / r1_range + 1 / r2_range) - ) - - @override - def initial_test_decades(self) -> Tuple[int, int]: - decade = ceil(log10(self.parallel_impedance.center())) - return decade, decade - - @override - def distance_to(self, spec: 'AmplifierValues') -> List[float]: - if self.amplification in spec.amplification and self.parallel_impedance in spec.parallel_impedance: - return [] - else: - return [ - abs(self.amplification.center() - spec.amplification.center()), - abs(self.parallel_impedance.center() - spec.parallel_impedance.center()) - ] - - @override - def intersects(self, spec: 'AmplifierValues') -> bool: - return self.amplification.intersects(spec.amplification) and \ - self.parallel_impedance.intersects(spec.parallel_impedance) + """Opamp follower circuit, outputs the same signal as the input (but probably stronger).""" + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name == "edg_importable:Follower" + return {"1": self.input, "3": self.output, "V+": self.pwr, "V-": self.gnd} + + def __init__(self) -> None: + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input = self.Port(AnalogSink.empty(), [Input]) + self.output = self.Port(AnalogSource.empty(), [Output]) + + @override + def contents(self) -> None: + super().contents() + + self.amp = self.Block(Opamp()) + self.forced = self.Block(ForcedAnalogSignal(self.input.link().signal)) + + self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch")) + + +class AmplifierValues(ESeriesRatioValue["AmplifierValues"]): + def __init__(self, amplification: Range, parallel_impedance: Range): + self.amplification = amplification # amplification factor from in to out + self.parallel_impedance = parallel_impedance # parallel impedance into the opamp negative pin + + @staticmethod + @override + def from_resistors(r1_range: Range, r2_range: Range) -> "AmplifierValues": + """r2 is the low-side resistor (Vin- to GND) and r1 is the high-side resistor (Vin- to Vout).""" + return AmplifierValues((r1_range / r2_range) + 1, 1 / (1 / r1_range + 1 / r2_range)) + + @override + def initial_test_decades(self) -> Tuple[int, int]: + decade = ceil(log10(self.parallel_impedance.center())) + return decade, decade + + @override + def distance_to(self, spec: "AmplifierValues") -> List[float]: + if self.amplification in spec.amplification and self.parallel_impedance in spec.parallel_impedance: + return [] + else: + return [ + abs(self.amplification.center() - spec.amplification.center()), + abs(self.parallel_impedance.center() - spec.parallel_impedance.center()), + ] + + @override + def intersects(self, spec: "AmplifierValues") -> bool: + return self.amplification.intersects(spec.amplification) and self.parallel_impedance.intersects( + spec.parallel_impedance + ) class Amplifier(OpampApplication, KiCadSchematicBlock, KiCadImportableBlock, GeneratorBlock): - """Opamp non-inverting amplifier, outputs a scaled-up version of the input signal. - - From https://en.wikipedia.org/wiki/Operational_amplifier_applications#Non-inverting_amplifier: - Vout = Vin (1 + R1/R2) - - The input and output impedances given are a bit more complex, so this simplifies it to - the opamp's specified pin impedances - TODO: is this correct(ish)? - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - mapping: Dict[str, Dict[str, BasePort]] = { - 'Simulation_SPICE:OPAMP': { - '+': self.input, '-': self.reference, '3': self.output, 'V+': self.pwr, 'V-': self.gnd - }, - 'edg_importable:Amplifier': { - '+': self.input, 'R': self.reference, '3': self.output, 'V+': self.pwr, 'V-': self.gnd - } - } - return mapping[symbol_name] - - def __init__(self, amplification: RangeLike, impedance: RangeLike = (10, 100)*kOhm, *, - series: IntLike = 24, tolerance: FloatLike = 0.01): # to be overridden by refinements - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input = self.Port(AnalogSink.empty(), [Input]) - self.output = self.Port(AnalogSource.empty(), [Output]) - self.reference = self.Port(AnalogSink.empty(), optional=True) # optional zero reference, defaults to GND - - self.amplification = self.ArgParameter(amplification) - self.impedance = self.ArgParameter(impedance) - self.series = self.ArgParameter(series) - self.tolerance = self.ArgParameter(tolerance) - self.generator_param(self.amplification, self.impedance, self.series, self.tolerance, self.reference.is_connected()) - - self.actual_amplification = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "amplification: ", DescriptionString.FormatUnits(self.actual_amplification, ""), - " of spec: ", DescriptionString.FormatUnits(self.amplification, "") - ) - - @override - def generate(self) -> None: - super().generate() - - calculator = ESeriesRatioUtil(ESeriesUtil.SERIES[self.get(self.series)], self.get(self.tolerance), AmplifierValues) - top_resistance, bottom_resistance = calculator.find(AmplifierValues(self.get(self.amplification), self.get(self.impedance))) - - self.amp = self.Block(Opamp()) # needed as forward reference for modeling - self.r1 = self.Block(Resistor(Range.from_tolerance(top_resistance, self.get(self.tolerance)))) - self.r2 = self.Block(Resistor(Range.from_tolerance(bottom_resistance, self.get(self.tolerance)))) - - if self.get(self.reference.is_connected()): - reference_type: CircuitPort = AnalogSink( - impedance=self.r1.actual_resistance + self.r2.actual_resistance - ) - reference_node: CircuitPort = self.reference - reference_range = self.reference.link().signal - else: - reference_type = Ground() - reference_node = self.gnd - reference_range = self.gnd.link().voltage - - input_signal_range = self.amp.out.voltage_out.intersect(self.input.link().signal - reference_range) - output_range = input_signal_range * self.actual_amplification + reference_range - # TODO tolerances can cause the range to be much larger than actual, so bound it to avoid false-positives - self.forced = self.Block(ForcedAnalogSignal(self.amp.out.signal_out.intersect(output_range))) - - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'r1.1': AnalogSink( - impedance=self.r1.actual_resistance + self.r2.actual_resistance - ), - 'r1.2': AnalogSource( # this models the entire node - voltage_out=self.amp.out.voltage_out, - impedance=1/(1 / self.r1.actual_resistance + 1 / self.r2.actual_resistance) - ), - 'r2.1': AnalogSink(), # ideal - 'r2.2': reference_type - }, nodes={ - 'ref': reference_node - }) - - self.assign(self.actual_amplification, 1 + (self.r1.actual_resistance / self.r2.actual_resistance)) - - -class DifferentialValues(ESeriesRatioValue['DifferentialValues']): - def __init__(self, ratio: Range, input_impedance: Range): - self.ratio = ratio # ratio from difference between inputs to output - self.input_impedance = input_impedance # resistance of the input resistor - - @staticmethod - @override - def from_resistors(r1_range: Range, r2_range: Range) -> 'DifferentialValues': - """r1 is the input side resistance and r2 is the feedback or ground resistor.""" - return DifferentialValues( - (r2_range / r1_range), - r1_range - ) - - @override - def initial_test_decades(self) -> Tuple[int, int]: - r1_decade = ceil(log10(self.input_impedance.center())) - r2_decade = ceil(log10((self.input_impedance * self.ratio).center())) - return r1_decade, r2_decade - - @override - def distance_to(self, spec: 'DifferentialValues') -> List[float]: - if self.ratio in spec.ratio and self.input_impedance in spec.input_impedance: - return [] - else: - return [ - abs(self.ratio.center() - spec.ratio.center()), - abs(self.input_impedance.center() - spec.input_impedance.center()) - ] - - @override - def intersects(self, spec: 'DifferentialValues') -> bool: - return self.ratio.intersects(spec.ratio) and \ - self.input_impedance.intersects(spec.input_impedance) + """Opamp non-inverting amplifier, outputs a scaled-up version of the input signal. + + From https://en.wikipedia.org/wiki/Operational_amplifier_applications#Non-inverting_amplifier: + Vout = Vin (1 + R1/R2) + + The input and output impedances given are a bit more complex, so this simplifies it to + the opamp's specified pin impedances - TODO: is this correct(ish)? + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + mapping: Dict[str, Dict[str, BasePort]] = { + "Simulation_SPICE:OPAMP": { + "+": self.input, + "-": self.reference, + "3": self.output, + "V+": self.pwr, + "V-": self.gnd, + }, + "edg_importable:Amplifier": { + "+": self.input, + "R": self.reference, + "3": self.output, + "V+": self.pwr, + "V-": self.gnd, + }, + } + return mapping[symbol_name] + + def __init__( + self, + amplification: RangeLike, + impedance: RangeLike = (10, 100) * kOhm, + *, + series: IntLike = 24, + tolerance: FloatLike = 0.01, + ): # to be overridden by refinements + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input = self.Port(AnalogSink.empty(), [Input]) + self.output = self.Port(AnalogSource.empty(), [Output]) + self.reference = self.Port(AnalogSink.empty(), optional=True) # optional zero reference, defaults to GND + + self.amplification = self.ArgParameter(amplification) + self.impedance = self.ArgParameter(impedance) + self.series = self.ArgParameter(series) + self.tolerance = self.ArgParameter(tolerance) + self.generator_param( + self.amplification, self.impedance, self.series, self.tolerance, self.reference.is_connected() + ) + + self.actual_amplification = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "amplification: ", + DescriptionString.FormatUnits(self.actual_amplification, ""), + " of spec: ", + DescriptionString.FormatUnits(self.amplification, ""), + ) + + @override + def generate(self) -> None: + super().generate() + + calculator = ESeriesRatioUtil( + ESeriesUtil.SERIES[self.get(self.series)], self.get(self.tolerance), AmplifierValues + ) + top_resistance, bottom_resistance = calculator.find( + AmplifierValues(self.get(self.amplification), self.get(self.impedance)) + ) + + self.amp = self.Block(Opamp()) # needed as forward reference for modeling + self.r1 = self.Block(Resistor(Range.from_tolerance(top_resistance, self.get(self.tolerance)))) + self.r2 = self.Block(Resistor(Range.from_tolerance(bottom_resistance, self.get(self.tolerance)))) + + if self.get(self.reference.is_connected()): + reference_type: CircuitPort = AnalogSink(impedance=self.r1.actual_resistance + self.r2.actual_resistance) + reference_node: CircuitPort = self.reference + reference_range = self.reference.link().signal + else: + reference_type = Ground() + reference_node = self.gnd + reference_range = self.gnd.link().voltage + + input_signal_range = self.amp.out.voltage_out.intersect(self.input.link().signal - reference_range) + output_range = input_signal_range * self.actual_amplification + reference_range + # TODO tolerances can cause the range to be much larger than actual, so bound it to avoid false-positives + self.forced = self.Block(ForcedAnalogSignal(self.amp.out.signal_out.intersect(output_range))) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "r1.1": AnalogSink(impedance=self.r1.actual_resistance + self.r2.actual_resistance), + "r1.2": AnalogSource( # this models the entire node + voltage_out=self.amp.out.voltage_out, + impedance=1 / (1 / self.r1.actual_resistance + 1 / self.r2.actual_resistance), + ), + "r2.1": AnalogSink(), # ideal + "r2.2": reference_type, + }, + nodes={"ref": reference_node}, + ) + + self.assign(self.actual_amplification, 1 + (self.r1.actual_resistance / self.r2.actual_resistance)) + + +class DifferentialValues(ESeriesRatioValue["DifferentialValues"]): + def __init__(self, ratio: Range, input_impedance: Range): + self.ratio = ratio # ratio from difference between inputs to output + self.input_impedance = input_impedance # resistance of the input resistor + + @staticmethod + @override + def from_resistors(r1_range: Range, r2_range: Range) -> "DifferentialValues": + """r1 is the input side resistance and r2 is the feedback or ground resistor.""" + return DifferentialValues((r2_range / r1_range), r1_range) + + @override + def initial_test_decades(self) -> Tuple[int, int]: + r1_decade = ceil(log10(self.input_impedance.center())) + r2_decade = ceil(log10((self.input_impedance * self.ratio).center())) + return r1_decade, r2_decade + + @override + def distance_to(self, spec: "DifferentialValues") -> List[float]: + if self.ratio in spec.ratio and self.input_impedance in spec.input_impedance: + return [] + else: + return [ + abs(self.ratio.center() - spec.ratio.center()), + abs(self.input_impedance.center() - spec.input_impedance.center()), + ] + + @override + def intersects(self, spec: "DifferentialValues") -> bool: + return self.ratio.intersects(spec.ratio) and self.input_impedance.intersects(spec.input_impedance) class DifferentialAmplifier(OpampApplication, KiCadSchematicBlock, KiCadImportableBlock, GeneratorBlock): - """Opamp differential amplifier, outputs the difference between the input nodes, scaled by some factor, - and offset from some reference node. - This implementation uses the same resistance for the two input resistors (R1, R2), - and the same resistance for the feedback and reference resistors (Rf, Rg). - From https://en.wikipedia.org/wiki/Operational_amplifier_applications#Differential_amplifier_(difference_amplifier): - Vout = Rf/R1 * (Vp - Vn) - - Impedance equations from https://e2e.ti.com/blogs_/archives/b/precisionhub/posts/overlooking-the-obvious-the-input-impedance-of-a-difference-amplifier - (ignoring the opamp input impedances, which we assume are >> the resistors) - Rin,n = R1 / (1 - (Rg / (R2+Rg)) * (Vin,n / Vin,p)) - Rin,p = R2 + Rg - Rout = opamp output impedance - TODO: is this correct? - - ratio specifies Rf/R1, the amplification ratio. - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - mapping: Dict[str, Dict[str, BasePort]] = { - 'Simulation_SPICE:OPAMP': { # reference pin not supported - '+': self.input_positive, '-': self.input_negative, '3': self.output, - 'V+': self.pwr, 'V-': self.gnd - }, - 'edg_importable:DifferentialAmplifier': { - '+': self.input_positive, '-': self.input_negative, - 'R': self.output_reference, '3': self.output, - 'V+': self.pwr, 'V-': self.gnd - } - } - return mapping[symbol_name] - - def __init__(self, ratio: RangeLike, input_impedance: RangeLike, *, - series: IntLike = 24, tolerance: FloatLike = 0.01): - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input_positive = self.Port(AnalogSink.empty()) - self.input_negative = self.Port(AnalogSink.empty()) - self.output_reference = self.Port(AnalogSink.empty(), optional=True) # gnd by default - self.output = self.Port(AnalogSource.empty()) - - self.ratio = self.ArgParameter(ratio) - self.input_impedance = self.ArgParameter(input_impedance) - self.series = self.ArgParameter(series) - self.tolerance = self.ArgParameter(tolerance) - self.generator_param(self.ratio, self.input_impedance, self.series, self.tolerance, - self.output_reference.is_connected()) - - self.actual_ratio = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "ratio: ", DescriptionString.FormatUnits(self.actual_ratio, ""), - " of spec: ", DescriptionString.FormatUnits(self.ratio, "") - ) - - @override - def generate(self) -> None: - super().generate() - - calculator = ESeriesRatioUtil(ESeriesUtil.SERIES[self.get(self.series)], self.get(self.tolerance), DifferentialValues) - r1_resistance, rf_resistance = calculator.find(DifferentialValues(self.get(self.ratio), self.get(self.input_impedance))) - - self.amp = self.Block(Opamp()) - self.r1 = self.Block(Resistor(Range.from_tolerance(r1_resistance, self.get(self.tolerance)))) - self.r2 = self.Block(Resistor(Range.from_tolerance(r1_resistance, self.get(self.tolerance)))) - self.rf = self.Block(Resistor(Range.from_tolerance(rf_resistance, self.get(self.tolerance)))) - self.rg = self.Block(Resistor(Range.from_tolerance(rf_resistance, self.get(self.tolerance)))) - - if self.get(self.output_reference.is_connected()): - output_neg_signal = self.output_reference.link().signal - output_neg_voltage = self.output_reference.link().voltage - output_neg_node: CircuitPort = self.output_reference - else: - output_neg_voltage = output_neg_signal = self.gnd.link().voltage - output_neg_node = self.gnd.as_analog_source() - - input_diff_range = self.input_positive.link().signal - self.input_negative.link().signal - output_diff_range = input_diff_range * self.actual_ratio + output_neg_signal - # TODO tolerances can cause the range to be much larger than actual, so bound it to avoid false-positives - self.forced = self.Block(ForcedAnalogSignal(self.amp.out.signal_out.intersect(output_diff_range))) - - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'r1.1': AnalogSink( # TODO very simplified and probably very wrong - impedance=self.r1.actual_resistance + self.rf.actual_resistance - ), - 'r1.2': AnalogSource( # combined R1 and Rf resistance - voltage_out=ResistiveDivider.divider_output( - self.input_negative.link().voltage, self.amp.out.voltage_out, - ResistiveDivider.divider_ratio(self.r1.actual_resistance, self.rf.actual_resistance) - ), - impedance=1 / (1 / self.r1.actual_resistance + 1 / self.rf.actual_resistance) - ), - 'rf.2': AnalogSink(), # ideal - 'rf.1': AnalogSink( # TODO very simplified and probably very wrong - impedance=self.r1.actual_resistance + self.rf.actual_resistance - ), - 'r2.1': AnalogSink( - impedance=self.r2.actual_resistance + self.rg.actual_resistance - ), - 'r2.2': AnalogSource( # combined R2 and Rg resistance - voltage_out=ResistiveDivider.divider_output( - self.input_positive.link().voltage, output_neg_voltage, - ResistiveDivider.divider_ratio(self.r2.actual_resistance, self.rg.actual_resistance) - ), - impedance=1 / (1 / self.r2.actual_resistance + 1 / self.rg.actual_resistance) - ), - 'rg.2': AnalogSink(), # ideal - 'rg.1': AnalogSink( - impedance=self.r2.actual_resistance + self.rg.actual_resistance + """Opamp differential amplifier, outputs the difference between the input nodes, scaled by some factor, + and offset from some reference node. + This implementation uses the same resistance for the two input resistors (R1, R2), + and the same resistance for the feedback and reference resistors (Rf, Rg). + From https://en.wikipedia.org/wiki/Operational_amplifier_applications#Differential_amplifier_(difference_amplifier): + Vout = Rf/R1 * (Vp - Vn) + + Impedance equations from https://e2e.ti.com/blogs_/archives/b/precisionhub/posts/overlooking-the-obvious-the-input-impedance-of-a-difference-amplifier + (ignoring the opamp input impedances, which we assume are >> the resistors) + Rin,n = R1 / (1 - (Rg / (R2+Rg)) * (Vin,n / Vin,p)) + Rin,p = R2 + Rg + Rout = opamp output impedance - TODO: is this correct? + + ratio specifies Rf/R1, the amplification ratio. + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + mapping: Dict[str, Dict[str, BasePort]] = { + "Simulation_SPICE:OPAMP": { # reference pin not supported + "+": self.input_positive, + "-": self.input_negative, + "3": self.output, + "V+": self.pwr, + "V-": self.gnd, + }, + "edg_importable:DifferentialAmplifier": { + "+": self.input_positive, + "-": self.input_negative, + "R": self.output_reference, + "3": self.output, + "V+": self.pwr, + "V-": self.gnd, + }, + } + return mapping[symbol_name] + + def __init__( + self, ratio: RangeLike, input_impedance: RangeLike, *, series: IntLike = 24, tolerance: FloatLike = 0.01 + ): + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input_positive = self.Port(AnalogSink.empty()) + self.input_negative = self.Port(AnalogSink.empty()) + self.output_reference = self.Port(AnalogSink.empty(), optional=True) # gnd by default + self.output = self.Port(AnalogSource.empty()) + + self.ratio = self.ArgParameter(ratio) + self.input_impedance = self.ArgParameter(input_impedance) + self.series = self.ArgParameter(series) + self.tolerance = self.ArgParameter(tolerance) + self.generator_param( + self.ratio, self.input_impedance, self.series, self.tolerance, self.output_reference.is_connected() + ) + + self.actual_ratio = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "ratio: ", + DescriptionString.FormatUnits(self.actual_ratio, ""), + " of spec: ", + DescriptionString.FormatUnits(self.ratio, ""), + ) + + @override + def generate(self) -> None: + super().generate() + + calculator = ESeriesRatioUtil( + ESeriesUtil.SERIES[self.get(self.series)], self.get(self.tolerance), DifferentialValues + ) + r1_resistance, rf_resistance = calculator.find( + DifferentialValues(self.get(self.ratio), self.get(self.input_impedance)) + ) + + self.amp = self.Block(Opamp()) + self.r1 = self.Block(Resistor(Range.from_tolerance(r1_resistance, self.get(self.tolerance)))) + self.r2 = self.Block(Resistor(Range.from_tolerance(r1_resistance, self.get(self.tolerance)))) + self.rf = self.Block(Resistor(Range.from_tolerance(rf_resistance, self.get(self.tolerance)))) + self.rg = self.Block(Resistor(Range.from_tolerance(rf_resistance, self.get(self.tolerance)))) + + if self.get(self.output_reference.is_connected()): + output_neg_signal = self.output_reference.link().signal + output_neg_voltage = self.output_reference.link().voltage + output_neg_node: CircuitPort = self.output_reference + else: + output_neg_voltage = output_neg_signal = self.gnd.link().voltage + output_neg_node = self.gnd.as_analog_source() + + input_diff_range = self.input_positive.link().signal - self.input_negative.link().signal + output_diff_range = input_diff_range * self.actual_ratio + output_neg_signal + # TODO tolerances can cause the range to be much larger than actual, so bound it to avoid false-positives + self.forced = self.Block(ForcedAnalogSignal(self.amp.out.signal_out.intersect(output_diff_range))) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "r1.1": AnalogSink( # TODO very simplified and probably very wrong + impedance=self.r1.actual_resistance + self.rf.actual_resistance + ), + "r1.2": AnalogSource( # combined R1 and Rf resistance + voltage_out=ResistiveDivider.divider_output( + self.input_negative.link().voltage, + self.amp.out.voltage_out, + ResistiveDivider.divider_ratio(self.r1.actual_resistance, self.rf.actual_resistance), + ), + impedance=1 / (1 / self.r1.actual_resistance + 1 / self.rf.actual_resistance), + ), + "rf.2": AnalogSink(), # ideal + "rf.1": AnalogSink( # TODO very simplified and probably very wrong + impedance=self.r1.actual_resistance + self.rf.actual_resistance + ), + "r2.1": AnalogSink(impedance=self.r2.actual_resistance + self.rg.actual_resistance), + "r2.2": AnalogSource( # combined R2 and Rg resistance + voltage_out=ResistiveDivider.divider_output( + self.input_positive.link().voltage, + output_neg_voltage, + ResistiveDivider.divider_ratio(self.r2.actual_resistance, self.rg.actual_resistance), + ), + impedance=1 / (1 / self.r2.actual_resistance + 1 / self.rg.actual_resistance), + ), + "rg.2": AnalogSink(), # ideal + "rg.1": AnalogSink(impedance=self.r2.actual_resistance + self.rg.actual_resistance), + }, + nodes={"output_neg": output_neg_node}, ) - }, nodes={ - 'output_neg': output_neg_node - }) - self.assign(self.actual_ratio, self.rf.actual_resistance / self.r1.actual_resistance) + self.assign(self.actual_ratio, self.rf.actual_resistance / self.r1.actual_resistance) class IntegratorInverting(OpampApplication, KiCadSchematicBlock, KiCadImportableBlock): - """Opamp integrator, outputs the negative integral of the input signal, relative to some reference signal. - Will clip to the input voltage rails. - - From https://en.wikipedia.org/wiki/Operational_amplifier_applications#Inverting_integrator: - Vout = - 1/RC * int(Vin) (integrating over time) - - Series is lower and tolerance is higher because there's a cap involved - TODO - separate series for cap, and series and tolerance by decade? - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - mapping: Dict[str, Dict[str, BasePort]] = { - 'Simulation_SPICE:OPAMP': { - '+': self.input, '-': self.reference, '3': self.output, 'V+': self.pwr, 'V-': self.gnd - }, - 'edg_importable:IntegratorInverting': { - '-': self.input, 'R': self.reference, '3': self.output, 'V+': self.pwr, 'V-': self.gnd - } - } - return mapping[symbol_name] - - def __init__(self, factor: RangeLike, capacitance: RangeLike, *, - series: IntLike = 6, tolerance: FloatLike = 0.05): - super().__init__() - - self.amp = self.Block(Opamp()) - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input = self.Port(AnalogSink.empty()) - self.output = self.Port(AnalogSource.empty()) - self.reference = self.Port(AnalogSink.empty()) # negative reference for the input and output signals - - self.factor = self.ArgParameter(factor) # output scale factor, 1/RC in units of 1/s - self.capacitance = self.ArgParameter(capacitance) - - self.actual_factor = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "factor: ", DescriptionString.FormatUnits(self.actual_factor, ""), - " of spec: ", DescriptionString.FormatUnits(self.factor, "") - ) - - self.r = self.Block(Resistor((1/self.factor).shrink_multiply(1/self.capacitance))) - self.c = self.Block(Capacitor( - capacitance=self.capacitance, - voltage=self.output.link().voltage - )) - - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'r.1': AnalogSink( # TODO very simplified and probably very wrong - impedance=self.r.actual_resistance - ), - 'c.1': AnalogSink(), # TODO impedance of the feedback circuit? - - 'r.2': AnalogSource( - voltage_out=self.amp.out.voltage_out, - impedance=self.r.actual_resistance - ), - 'c.2': AnalogSink(), - }) - - self.assign(self.actual_factor, 1 / self.r.actual_resistance / self.c.actual_capacitance) + """Opamp integrator, outputs the negative integral of the input signal, relative to some reference signal. + Will clip to the input voltage rails. + + From https://en.wikipedia.org/wiki/Operational_amplifier_applications#Inverting_integrator: + Vout = - 1/RC * int(Vin) (integrating over time) + + Series is lower and tolerance is higher because there's a cap involved + TODO - separate series for cap, and series and tolerance by decade? + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + mapping: Dict[str, Dict[str, BasePort]] = { + "Simulation_SPICE:OPAMP": { + "+": self.input, + "-": self.reference, + "3": self.output, + "V+": self.pwr, + "V-": self.gnd, + }, + "edg_importable:IntegratorInverting": { + "-": self.input, + "R": self.reference, + "3": self.output, + "V+": self.pwr, + "V-": self.gnd, + }, + } + return mapping[symbol_name] + + def __init__(self, factor: RangeLike, capacitance: RangeLike, *, series: IntLike = 6, tolerance: FloatLike = 0.05): + super().__init__() + + self.amp = self.Block(Opamp()) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input = self.Port(AnalogSink.empty()) + self.output = self.Port(AnalogSource.empty()) + self.reference = self.Port(AnalogSink.empty()) # negative reference for the input and output signals + + self.factor = self.ArgParameter(factor) # output scale factor, 1/RC in units of 1/s + self.capacitance = self.ArgParameter(capacitance) + + self.actual_factor = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "factor: ", + DescriptionString.FormatUnits(self.actual_factor, ""), + " of spec: ", + DescriptionString.FormatUnits(self.factor, ""), + ) + + self.r = self.Block(Resistor((1 / self.factor).shrink_multiply(1 / self.capacitance))) + self.c = self.Block(Capacitor(capacitance=self.capacitance, voltage=self.output.link().voltage)) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "r.1": AnalogSink(impedance=self.r.actual_resistance), # TODO very simplified and probably very wrong + "c.1": AnalogSink(), # TODO impedance of the feedback circuit? + "r.2": AnalogSource(voltage_out=self.amp.out.voltage_out, impedance=self.r.actual_resistance), + "c.2": AnalogSink(), + }, + ) + + self.assign(self.actual_factor, 1 / self.r.actual_resistance / self.c.actual_capacitance) class SummingAmplifier(OpampApplication): - @classmethod - def calculate_ratio(cls, resistances: List[Range]) -> List[Range]: - """Calculates each input's contribution to the output, for 1x gain. - Non-inverting summing amplifier topology. - Based on https://www.electronicshub.org/summing-amplifier/, which calculates the voltage of each input - and uses superposition to combine them.""" - output = [] - for i, resistance in enumerate(resistances): - # compute the two tolerance corners - others = resistances[:i] + resistances[i+1:] - other_lowest_parallel = 1 / sum([1 / other.lower for other in others]) - other_highest_parallel = 1 / sum([1 / other.upper for other in others]) - # ratio is lowest when this resistance is highest and other is lowest - ratio_lowest = other_lowest_parallel / (resistance.upper + other_lowest_parallel) - # ratio is highest when this resistance is lowest and other is highest - ratio_highest = other_highest_parallel / (resistance.lower + other_highest_parallel) - - output.append(Range(ratio_lowest, ratio_highest)) - - return output + @classmethod + def calculate_ratio(cls, resistances: List[Range]) -> List[Range]: + """Calculates each input's contribution to the output, for 1x gain. + Non-inverting summing amplifier topology. + Based on https://www.electronicshub.org/summing-amplifier/, which calculates the voltage of each input + and uses superposition to combine them.""" + output = [] + for i, resistance in enumerate(resistances): + # compute the two tolerance corners + others = resistances[:i] + resistances[i + 1 :] + other_lowest_parallel = 1 / sum([1 / other.lower for other in others]) + other_highest_parallel = 1 / sum([1 / other.upper for other in others]) + # ratio is lowest when this resistance is highest and other is lowest + ratio_lowest = other_lowest_parallel / (resistance.upper + other_lowest_parallel) + # ratio is highest when this resistance is lowest and other is highest + ratio_highest = other_highest_parallel / (resistance.lower + other_highest_parallel) + + output.append(Range(ratio_lowest, ratio_highest)) + + return output diff --git a/edg/abstract_parts/OpampCurrentSensor.py b/edg/abstract_parts/OpampCurrentSensor.py index fd2f8c576..5b0ab7948 100644 --- a/edg/abstract_parts/OpampCurrentSensor.py +++ b/edg/abstract_parts/OpampCurrentSensor.py @@ -10,44 +10,38 @@ class OpampCurrentSensor(CurrentSensor, KiCadImportableBlock, Block): - """Current sensor block using a resistive sense element and an opamp-based differential amplifier. - For a positive current (flowing from pwr_in -> pwr_out), this generates a positive voltage on the output. - Output reference can be floating (eg, at Vdd/2) to allow bidirectional current sensing. - - Discrete diffamp circuits generally have poor accuracy as a result of resistor tolerances, including - very poor common-mode rejection. - """ - def __init__(self, resistance: RangeLike, ratio: RangeLike, input_impedance: RangeLike): - super().__init__() - - self.sense = self.Block(CurrentSenseResistor( - resistance=resistance - )) - self.pwr_in = self.Export(self.sense.pwr_in) - self.pwr_out = self.Export(self.sense.pwr_out) - - self.amp = self.Block(DifferentialAmplifier( - ratio=ratio, - input_impedance=input_impedance - )) - self.pwr = self.Export(self.amp.pwr, [Power]) - self.gnd = self.Export(self.amp.gnd, [Common]) - self.ref = self.Export(self.amp.output_reference) - self.out = self.Port(AnalogSource.empty()) - - - @override - def contents(self) -> None: - self.connect(self.amp.input_positive, self.sense.sense_in) - self.connect(self.amp.input_negative, self.sense.sense_out) - - output_swing = self.pwr_out.link().current_drawn * self.sense.actual_resistance * self.amp.actual_ratio - self.force_signal = self.Block(ForcedAnalogSignal(output_swing + self.ref.link().signal)) - self.connect(self.amp.output, self.force_signal.signal_in) - self.connect(self.force_signal.signal_out, self.out) - - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: - assert symbol_name == 'edg_importable:OpampCurrentSensor' - return {'+': self.pwr_in, '-': self.pwr_out, 'R': self.ref, '3': self.out, - 'V+': self.pwr, 'V-': self.gnd} + """Current sensor block using a resistive sense element and an opamp-based differential amplifier. + For a positive current (flowing from pwr_in -> pwr_out), this generates a positive voltage on the output. + Output reference can be floating (eg, at Vdd/2) to allow bidirectional current sensing. + + Discrete diffamp circuits generally have poor accuracy as a result of resistor tolerances, including + very poor common-mode rejection. + """ + + def __init__(self, resistance: RangeLike, ratio: RangeLike, input_impedance: RangeLike): + super().__init__() + + self.sense = self.Block(CurrentSenseResistor(resistance=resistance)) + self.pwr_in = self.Export(self.sense.pwr_in) + self.pwr_out = self.Export(self.sense.pwr_out) + + self.amp = self.Block(DifferentialAmplifier(ratio=ratio, input_impedance=input_impedance)) + self.pwr = self.Export(self.amp.pwr, [Power]) + self.gnd = self.Export(self.amp.gnd, [Common]) + self.ref = self.Export(self.amp.output_reference) + self.out = self.Port(AnalogSource.empty()) + + @override + def contents(self) -> None: + self.connect(self.amp.input_positive, self.sense.sense_in) + self.connect(self.amp.input_negative, self.sense.sense_out) + + output_swing = self.pwr_out.link().current_drawn * self.sense.actual_resistance * self.amp.actual_ratio + self.force_signal = self.Block(ForcedAnalogSignal(output_swing + self.ref.link().signal)) + self.connect(self.amp.output, self.force_signal.signal_in) + self.connect(self.force_signal.signal_out, self.out) + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]: + assert symbol_name == "edg_importable:OpampCurrentSensor" + return {"+": self.pwr_in, "-": self.pwr_out, "R": self.ref, "3": self.out, "V+": self.pwr, "V-": self.gnd} diff --git a/edg/abstract_parts/PartsTable.py b/edg/abstract_parts/PartsTable.py index 8bf4b5829..dfdd721d8 100644 --- a/edg/abstract_parts/PartsTable.py +++ b/edg/abstract_parts/PartsTable.py @@ -2,8 +2,22 @@ import csv import itertools -from typing import Generic, Type, overload, Union, Callable, List, Dict, Any, KeysView, Optional, \ - cast, Tuple, Sequence, Protocol +from typing import ( + Generic, + Type, + overload, + Union, + Callable, + List, + Dict, + Any, + KeysView, + Optional, + cast, + Tuple, + Sequence, + Protocol, +) from typing_extensions import ParamSpec, TypeVar, override @@ -12,240 +26,266 @@ # from https://stackoverflow.com/questions/47965083/comparable-types-with-mypy class Comparable(Protocol): - @override - def __eq__(self, other: Any) -> bool: ... - def __lt__(self, other: Any) -> bool: ... + @override + def __eq__(self, other: Any) -> bool: ... + def __lt__(self, other: Any) -> bool: ... + + +PartsTableColumnType = TypeVar("PartsTableColumnType", default=Any) -PartsTableColumnType = TypeVar('PartsTableColumnType', default=Any) class PartsTableColumn(Generic[PartsTableColumnType]): - """A column header for a parts table, that allows indexing by an object - (instead of a string) that also checks the value type. - Required for use in creating a new parts table entry. - """ - def __init__(self, value_type: Type[PartsTableColumnType]): - self.value_type = value_type + """A column header for a parts table, that allows indexing by an object + (instead of a string) that also checks the value type. + Required for use in creating a new parts table entry. + """ + + def __init__(self, value_type: Type[PartsTableColumnType]): + self.value_type = value_type class PartsTableRow: - """A row in the parts table. Immutable. - Internal type, does not do any error checking (so data should be checked before being - passed into this object).""" - def __init__(self, value: Dict[Any, Any]): # TODO Dict not covariant so we can't check key types - self.values = value - - @overload - def __getitem__(self, item: str) -> str: ... - @overload - def __getitem__(self, item: PartsTableColumn[PartsTableColumnType]) -> PartsTableColumnType: ... - - def __getitem__(self, item: Union[str, PartsTableColumn[PartsTableColumnType]]) -> \ - Union[str, PartsTableColumnType]: - value = self.values[item] - if isinstance(item, str): - assert isinstance(value, str) - return value - elif isinstance(item, PartsTableColumn): - assert isinstance(value, item.value_type) - return value - else: - raise TypeError() + """A row in the parts table. Immutable. + Internal type, does not do any error checking (so data should be checked before being + passed into this object).""" + + def __init__(self, value: Dict[Any, Any]): # TODO Dict not covariant so we can't check key types + self.values = value + + @overload + def __getitem__(self, item: str) -> str: ... + @overload + def __getitem__(self, item: PartsTableColumn[PartsTableColumnType]) -> PartsTableColumnType: ... + + def __getitem__(self, item: Union[str, PartsTableColumn[PartsTableColumnType]]) -> Union[str, PartsTableColumnType]: + value = self.values[item] + if isinstance(item, str): + assert isinstance(value, str) + return value + elif isinstance(item, PartsTableColumn): + assert isinstance(value, item.value_type) + return value + else: + raise TypeError() class PartsTable: - """A parts table, with data that can be loaded from a CSV, and providing functions for - filtering and transformation. - Immutable, all data is copied as needed (see functions for depth of copy). - - Optimization TODO: could probably significantly (~2x) improve performance using not-dicts - https://www.districtdatalabs.com/simple-csv-data-wrangling-with-python - """ - @staticmethod - def with_source_dir(filenames: List[str], subdir: Optional[str] = None) -> List[str]: - """Given a list of filenames, prepends the absolute path to the calling source file, with an optional subdir. - """ - from types import FrameType - import inspect - import os - - calling_filename = cast(FrameType, cast(FrameType, inspect.currentframe()).f_back).f_code.co_filename - prefix_dir = os.path.dirname(calling_filename) - if subdir is not None: - prefix_dir = os.path.join(prefix_dir, subdir) - return [os.path.join(prefix_dir, filename) for filename in filenames] - - @classmethod - def from_csv_files(cls, csv_names: List[str], encoding: str='utf-8') -> 'PartsTable': - dict_rows = [] - for filename in csv_names: - with open(filename, newline='', encoding=encoding) as csvfile: - reader = csv.DictReader(csvfile) - dict_rows.extend([row for row in reader]) - return cls.from_dict_rows(dict_rows) - - @staticmethod - def from_dict_rows(*dict_rowss: Union[List[Dict[str, str]], List[Dict[str, str]]]) -> 'PartsTable': - """Creates a parts table from dict rows, such as parsed by csv.DictReader. - Checks to make sure all incoming rows are dense (have all cells).""" - all_dict_rows = list(itertools.chain(*dict_rowss)) - - if len(all_dict_rows) > 1: # if nonempty, check for consistency - first_keys = set(all_dict_rows[0].keys()) - for dict_row in all_dict_rows[1:]: - difference = first_keys.symmetric_difference(set(dict_row.keys())) - assert not difference, f"table has different keys: {difference}" - rows = [PartsTableRow(dict_row) for dict_row in all_dict_rows] - return PartsTable(rows) - - def __len__(self) -> int: - return len(self.rows) - - def __init__(self, rows: List[PartsTableRow]): - """Internal function, just creates a new PartsTable wrapping the rows, without any checking.""" - self.rows = rows - - def filter(self, fn: Callable[[PartsTableRow], bool]) -> PartsTable: - """Creates a new table view (shallow copy) with rows filtered according to some criteria.""" - new_rows = [row for row in self.rows - if fn(row)] - return PartsTable(new_rows) - - def map_new_columns(self, fn: Callable[[PartsTableRow], Optional[Dict[PartsTableColumn, Any]]], - overwrite: bool = False) -> PartsTable: - """Creates a new table (deep copy) with additional rows. - All entries must have the same keys. - Specify overwrite=True to overwrite an existing row. To preserve the existing value (no-op), return it. - Return None to drop the row.""" - new_rows: List[PartsTableRow] = [] - first_keys: Optional[KeysView[PartsTableColumn]] = None - for row in self.rows: - new_columns = fn(row) - if new_columns is None: - continue - - if first_keys is None: - first_keys = new_columns.keys() - assert first_keys.isdisjoint(row.values.keys()) or overwrite, \ - f"new columns {new_columns} overwrites existing row keys {row.values.keys()} without overwrite=True" - else: - assert first_keys == new_columns.keys(), \ - f"new columns {new_columns} in row {row} has different keys than first row keys {first_keys}" - for new_col_key, new_col_val in new_columns.items(): - assert isinstance(new_col_key, PartsTableColumn), \ - f"new column key {new_col_key} in {row} not of type PartsTableColumn" - assert isinstance(new_col_val, new_col_key.value_type), \ - f"new column elt {new_col_key}={new_col_val} in {row} not of expected type {new_col_key.value_type}" - new_row_dict = {} - new_row_dict.update(row.values) - new_row_dict.update(new_columns) - new_rows.append(PartsTableRow(new_row_dict)) - return PartsTable(new_rows) - - MapType = TypeVar('MapType') - def map(self, fn: Callable[[PartsTableRow], MapType]) -> List[MapType]: - """Applies a transformation function to every row and returns the results as a list.""" - output = [] - for row in self.rows: - output.append(fn(row)) - return output - - ComparableType = TypeVar('ComparableType', bound=Comparable) - def sort_by(self, fn: Callable[[PartsTableRow], ComparableType], reverse: bool = False) -> PartsTable: - """Creates a new table view (shallow copy) with rows sorted in some order. - - TODO this should support Comparable, but that's not a builtin protocol :( + """A parts table, with data that can be loaded from a CSV, and providing functions for + filtering and transformation. + Immutable, all data is copied as needed (see functions for depth of copy). + + Optimization TODO: could probably significantly (~2x) improve performance using not-dicts + https://www.districtdatalabs.com/simple-csv-data-wrangling-with-python """ - new_rows = sorted(self.rows, key=fn, reverse=reverse) - return PartsTable(new_rows) - def first(self, err: str="no elements in list") -> PartsTableRow: - if not self.rows: - raise IndexError(err) - return self.rows[0] + @staticmethod + def with_source_dir(filenames: List[str], subdir: Optional[str] = None) -> List[str]: + """Given a list of filenames, prepends the absolute path to the calling source file, with an optional subdir.""" + from types import FrameType + import inspect + import os + + calling_filename = cast(FrameType, cast(FrameType, inspect.currentframe()).f_back).f_code.co_filename + prefix_dir = os.path.dirname(calling_filename) + if subdir is not None: + prefix_dir = os.path.join(prefix_dir, subdir) + return [os.path.join(prefix_dir, filename) for filename in filenames] + + @classmethod + def from_csv_files(cls, csv_names: List[str], encoding: str = "utf-8") -> "PartsTable": + dict_rows = [] + for filename in csv_names: + with open(filename, newline="", encoding=encoding) as csvfile: + reader = csv.DictReader(csvfile) + dict_rows.extend([row for row in reader]) + return cls.from_dict_rows(dict_rows) + + @staticmethod + def from_dict_rows(*dict_rowss: Union[List[Dict[str, str]], List[Dict[str, str]]]) -> "PartsTable": + """Creates a parts table from dict rows, such as parsed by csv.DictReader. + Checks to make sure all incoming rows are dense (have all cells).""" + all_dict_rows = list(itertools.chain(*dict_rowss)) + + if len(all_dict_rows) > 1: # if nonempty, check for consistency + first_keys = set(all_dict_rows[0].keys()) + for dict_row in all_dict_rows[1:]: + difference = first_keys.symmetric_difference(set(dict_row.keys())) + assert not difference, f"table has different keys: {difference}" + rows = [PartsTableRow(dict_row) for dict_row in all_dict_rows] + return PartsTable(rows) + + def __len__(self) -> int: + return len(self.rows) + + def __init__(self, rows: List[PartsTableRow]): + """Internal function, just creates a new PartsTable wrapping the rows, without any checking.""" + self.rows = rows + + def filter(self, fn: Callable[[PartsTableRow], bool]) -> PartsTable: + """Creates a new table view (shallow copy) with rows filtered according to some criteria.""" + new_rows = [row for row in self.rows if fn(row)] + return PartsTable(new_rows) + + def map_new_columns( + self, fn: Callable[[PartsTableRow], Optional[Dict[PartsTableColumn, Any]]], overwrite: bool = False + ) -> PartsTable: + """Creates a new table (deep copy) with additional rows. + All entries must have the same keys. + Specify overwrite=True to overwrite an existing row. To preserve the existing value (no-op), return it. + Return None to drop the row.""" + new_rows: List[PartsTableRow] = [] + first_keys: Optional[KeysView[PartsTableColumn]] = None + for row in self.rows: + new_columns = fn(row) + if new_columns is None: + continue + + if first_keys is None: + first_keys = new_columns.keys() + assert ( + first_keys.isdisjoint(row.values.keys()) or overwrite + ), f"new columns {new_columns} overwrites existing row keys {row.values.keys()} without overwrite=True" + else: + assert ( + first_keys == new_columns.keys() + ), f"new columns {new_columns} in row {row} has different keys than first row keys {first_keys}" + for new_col_key, new_col_val in new_columns.items(): + assert isinstance( + new_col_key, PartsTableColumn + ), f"new column key {new_col_key} in {row} not of type PartsTableColumn" + assert isinstance( + new_col_val, new_col_key.value_type + ), f"new column elt {new_col_key}={new_col_val} in {row} not of expected type {new_col_key.value_type}" + new_row_dict = {} + new_row_dict.update(row.values) + new_row_dict.update(new_columns) + new_rows.append(PartsTableRow(new_row_dict)) + return PartsTable(new_rows) + + MapType = TypeVar("MapType") + + def map(self, fn: Callable[[PartsTableRow], MapType]) -> List[MapType]: + """Applies a transformation function to every row and returns the results as a list.""" + output = [] + for row in self.rows: + output.append(fn(row)) + return output + + ComparableType = TypeVar("ComparableType", bound=Comparable) + + def sort_by(self, fn: Callable[[PartsTableRow], ComparableType], reverse: bool = False) -> PartsTable: + """Creates a new table view (shallow copy) with rows sorted in some order. + + TODO this should support Comparable, but that's not a builtin protocol :( + """ + new_rows = sorted(self.rows, key=fn, reverse=reverse) + return PartsTable(new_rows) + + def first(self, err: str = "no elements in list") -> PartsTableRow: + if not self.rows: + raise IndexError(err) + return self.rows[0] + + +UserFnMetaParams = ParamSpec("UserFnMetaParams") +UserFnType = TypeVar("UserFnType", bound=Callable[..., Any], covariant=True) -UserFnMetaParams = ParamSpec('UserFnMetaParams') -UserFnType = TypeVar('UserFnType', bound=Callable[..., Any], covariant=True) class UserFnSerialiable(Protocol[UserFnMetaParams, UserFnType]): - """A protocol that marks functions as usable in deserialize, that they have been registered.""" - _is_serializable: None # guard attribute + """A protocol that marks functions as usable in deserialize, that they have been registered.""" + + _is_serializable: None # guard attribute + + def __call__(self, *args: UserFnMetaParams.args, **kwargs: UserFnMetaParams.kwargs) -> UserFnType: ... - def __call__(self, *args: UserFnMetaParams.args, **kwargs: UserFnMetaParams.kwargs) -> UserFnType: ... - __name__: str + __name__: str class ExperimentalUserFnPartsTable(PartsTable): - """A PartsTable that can take in a user-defined function for filtering and (possibly) other operations. - These functions are serialized to a string by an internal name (cannot execute arbitrary code, - bounded to defined functions in the codebase), and some arguments can be serialized with the name - (think partial(...)). - Functions must be pre-registered using the @ExperimentalUserFnPartsTable.user_fn(...) decorator, - non-pre-registered functions will not be available. - - This is intended to support searches on parts tables that are cross-coupled across multiple parameters, - but still restricted to within on table (e.g., no cross-optimizing RC filters). - - EXPERIMENTAL - subject to change without notice.""" - - _FN_SERIALIZATION_SEPARATOR = ";" - - _user_fns: Dict[str, Tuple[Callable[..., Any], Sequence[Type[Any]]]] = {} # name -> fn, [arg types] - _fn_name_dict: Dict[Callable[..., Any], str] = {} - - @staticmethod - def user_fn(param_types: Sequence[Type[Any]] = []) -> Callable[[Callable[UserFnMetaParams, UserFnType]], - UserFnSerialiable[UserFnMetaParams, UserFnType]]: - def decorator(fn: Callable[UserFnMetaParams, UserFnType]) -> UserFnSerialiable[UserFnMetaParams, UserFnType]: - """Decorator to register a user function that can be used in ExperimentalUserFnPartsTable.""" - if fn.__name__ in ExperimentalUserFnPartsTable._user_fns or fn in ExperimentalUserFnPartsTable._fn_name_dict: - raise ValueError(f"Function {fn.__name__} already registered.") - ExperimentalUserFnPartsTable._user_fns[fn.__name__] = (fn, param_types) - ExperimentalUserFnPartsTable._fn_name_dict[fn] = fn.__name__ - return fn # type: ignore - return decorator - - @classmethod - def serialize_fn(cls, fn: UserFnSerialiable[UserFnMetaParams, UserFnType], - *args: UserFnMetaParams.args, **kwargs: UserFnMetaParams.kwargs) -> str: - """Serializes a user function to a string.""" - assert not kwargs, "kwargs not supported in serialization" - if fn not in cls._fn_name_dict: - raise ValueError(f"Function {fn} not registered.") - fn_ctor, fn_argtypes = cls._user_fns[fn.__name__] - def serialize_arg(tpe: Type[Any], val: Any) -> str: - assert isinstance(val, tpe), f"in serialize {val}, expected {tpe}, got {type(val)}" - if tpe is bool: - return str(val) - elif tpe is int: - return str(val) - elif tpe is float: - return str(val) - elif tpe is Range: - return f"({val.lower},{val.upper})" - else: - raise TypeError(f"cannot serialize type {tpe} in user function serialization") - serialized_args = [serialize_arg(tpe, arg) for tpe, arg in zip(fn_argtypes, args)] - return cls._FN_SERIALIZATION_SEPARATOR.join([fn.__name__] + serialized_args) - - @classmethod - def deserialize_fn(cls, serialized: str) -> Any: - """Deserializes and applies a user function from a string.""" - split = serialized.split(cls._FN_SERIALIZATION_SEPARATOR) - if split[0] not in cls._user_fns: - raise ValueError(f"Function {serialized} not registered.") - fn_ctor, fn_argtypes = cls._user_fns[split[0]] - assert len(split) == len(fn_argtypes) + 1 - def deserialize_arg(tpe: Type[Any], val: str) -> Any: - if tpe is bool: - return val == 'True' - elif tpe is int: - return int(val) - elif tpe is float: - return float(val) - elif tpe is Range: - parts = val[1:-1].split(",") - return Range(float(parts[0]), float(parts[1])) - else: - raise TypeError(f"cannot deserialize type {tpe} in user function serialization") - deserialized_args = [deserialize_arg(tpe, arg) for tpe, arg in zip(fn_argtypes, split[1:])] - return fn_ctor(*deserialized_args) + """A PartsTable that can take in a user-defined function for filtering and (possibly) other operations. + These functions are serialized to a string by an internal name (cannot execute arbitrary code, + bounded to defined functions in the codebase), and some arguments can be serialized with the name + (think partial(...)). + Functions must be pre-registered using the @ExperimentalUserFnPartsTable.user_fn(...) decorator, + non-pre-registered functions will not be available. + + This is intended to support searches on parts tables that are cross-coupled across multiple parameters, + but still restricted to within on table (e.g., no cross-optimizing RC filters). + + EXPERIMENTAL - subject to change without notice.""" + + _FN_SERIALIZATION_SEPARATOR = ";" + + _user_fns: Dict[str, Tuple[Callable[..., Any], Sequence[Type[Any]]]] = {} # name -> fn, [arg types] + _fn_name_dict: Dict[Callable[..., Any], str] = {} + + @staticmethod + def user_fn( + param_types: Sequence[Type[Any]] = [], + ) -> Callable[[Callable[UserFnMetaParams, UserFnType]], UserFnSerialiable[UserFnMetaParams, UserFnType]]: + def decorator(fn: Callable[UserFnMetaParams, UserFnType]) -> UserFnSerialiable[UserFnMetaParams, UserFnType]: + """Decorator to register a user function that can be used in ExperimentalUserFnPartsTable.""" + if ( + fn.__name__ in ExperimentalUserFnPartsTable._user_fns + or fn in ExperimentalUserFnPartsTable._fn_name_dict + ): + raise ValueError(f"Function {fn.__name__} already registered.") + ExperimentalUserFnPartsTable._user_fns[fn.__name__] = (fn, param_types) + ExperimentalUserFnPartsTable._fn_name_dict[fn] = fn.__name__ + return fn # type: ignore + + return decorator + + @classmethod + def serialize_fn( + cls, + fn: UserFnSerialiable[UserFnMetaParams, UserFnType], + *args: UserFnMetaParams.args, + **kwargs: UserFnMetaParams.kwargs, + ) -> str: + """Serializes a user function to a string.""" + assert not kwargs, "kwargs not supported in serialization" + if fn not in cls._fn_name_dict: + raise ValueError(f"Function {fn} not registered.") + fn_ctor, fn_argtypes = cls._user_fns[fn.__name__] + + def serialize_arg(tpe: Type[Any], val: Any) -> str: + assert isinstance(val, tpe), f"in serialize {val}, expected {tpe}, got {type(val)}" + if tpe is bool: + return str(val) + elif tpe is int: + return str(val) + elif tpe is float: + return str(val) + elif tpe is Range: + return f"({val.lower},{val.upper})" + else: + raise TypeError(f"cannot serialize type {tpe} in user function serialization") + + serialized_args = [serialize_arg(tpe, arg) for tpe, arg in zip(fn_argtypes, args)] + return cls._FN_SERIALIZATION_SEPARATOR.join([fn.__name__] + serialized_args) + + @classmethod + def deserialize_fn(cls, serialized: str) -> Any: + """Deserializes and applies a user function from a string.""" + split = serialized.split(cls._FN_SERIALIZATION_SEPARATOR) + if split[0] not in cls._user_fns: + raise ValueError(f"Function {serialized} not registered.") + fn_ctor, fn_argtypes = cls._user_fns[split[0]] + assert len(split) == len(fn_argtypes) + 1 + + def deserialize_arg(tpe: Type[Any], val: str) -> Any: + if tpe is bool: + return val == "True" + elif tpe is int: + return int(val) + elif tpe is float: + return float(val) + elif tpe is Range: + parts = val[1:-1].split(",") + return Range(float(parts[0]), float(parts[1])) + else: + raise TypeError(f"cannot deserialize type {tpe} in user function serialization") + + deserialized_args = [deserialize_arg(tpe, arg) for tpe, arg in zip(fn_argtypes, split[1:])] + return fn_ctor(*deserialized_args) diff --git a/edg/abstract_parts/PartsTablePart.py b/edg/abstract_parts/PartsTablePart.py index 256d1fbe2..1b10b2083 100644 --- a/edg/abstract_parts/PartsTablePart.py +++ b/edg/abstract_parts/PartsTablePart.py @@ -9,121 +9,129 @@ class PartsTableBase: - """A base class defining infrastructure for building a (cached) parts table. - Subclasses should implement _make_table, which returns the underlying parts table. - Additional filtering can be done by the generator.""" - _TABLE: Optional[PartsTable] = None - - # These need to be implemented by the part table - PART_NUMBER_COL: Union[str, PartsTableColumn[str]] - MANUFACTURER_COL: Union[str, PartsTableColumn[str]] - DESCRIPTION_COL: Union[str, PartsTableColumn[str]] - DATASHEET_COL: Union[str, PartsTableColumn[str]] - - @classmethod - @abstractmethod - def _make_table(cls) -> PartsTable: - """Returns a parts table for this device. Implement me.""" - ... - - @classmethod - def _get_table(cls) -> PartsTable: - if cls._TABLE is None: - cls._TABLE = cls._make_table() - if len(cls._TABLE) == 0: - raise ValueError(f"{cls.__name__} _make_table returned empty table") - return cls._TABLE + """A base class defining infrastructure for building a (cached) parts table. + Subclasses should implement _make_table, which returns the underlying parts table. + Additional filtering can be done by the generator.""" + + _TABLE: Optional[PartsTable] = None + + # These need to be implemented by the part table + PART_NUMBER_COL: Union[str, PartsTableColumn[str]] + MANUFACTURER_COL: Union[str, PartsTableColumn[str]] + DESCRIPTION_COL: Union[str, PartsTableColumn[str]] + DATASHEET_COL: Union[str, PartsTableColumn[str]] + + @classmethod + @abstractmethod + def _make_table(cls) -> PartsTable: + """Returns a parts table for this device. Implement me.""" + ... + + @classmethod + def _get_table(cls) -> PartsTable: + if cls._TABLE is None: + cls._TABLE = cls._make_table() + if len(cls._TABLE) == 0: + raise ValueError(f"{cls.__name__} _make_table returned empty table") + return cls._TABLE @abstract_block class PartsTablePart(Block): - """An interface mixin for a part that is selected from a table, defining parameters to allow manual part selection - as well as matching parts.""" - def __init__(self, *args: Any, part: StringLike = "", **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.part = self.ArgParameter(part) - self.actual_part = self.Parameter(StringExpr()) - self.matching_parts = self.Parameter(ArrayStringExpr()) + """An interface mixin for a part that is selected from a table, defining parameters to allow manual part selection + as well as matching parts.""" + + def __init__(self, *args: Any, part: StringLike = "", **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.part = self.ArgParameter(part) + self.actual_part = self.Parameter(StringExpr()) + self.matching_parts = self.Parameter(ArrayStringExpr()) @non_library class PartsTableSelector(PartsTablePart, GeneratorBlock, PartsTableBase): - """PartsTablePart that includes the parts selection framework logic. - Subclasses only need to extend _row_filter and _row_generate with part-specific logic.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.part) - - def _row_filter(self, row: PartsTableRow) -> bool: - """Returns whether the candidate row satisfies the requirements (should be kept). - Only called within generate(), so has access to GeneratorParam.get(). - Subclasses should chain this by and-ing with a super() call.""" - return not self.get(self.part) or (self.get(self.part) == row[self.PART_NUMBER_COL]) - - def _table_postprocess(self, table: PartsTable) -> PartsTable: - """Optional postprocessing step that takes a table and returns a transformed table. - Only called within generate(), so has access to GeneratorParam.get(). - Subclasses should chain with the results of a super() call.""" - return table - - @classmethod - def _row_sort_by(cls, row: PartsTableRow) -> Any: - """Defines an optional sorting key for rows of this parts table.""" - return [] - - def _row_generate(self, row: PartsTableRow) -> None: - """Once a row is selected, this is called to generate the implementation given the part selection. - Subclasses super chain this with a super() call. - If there is no matching row, this is not called.""" - self.assign(self.actual_part, row[self.PART_NUMBER_COL]) - - @override - def generate(self) -> None: - matching_table = self._get_table().filter(lambda row: self._row_filter(row)) - postprocessed_table = self._table_postprocess(matching_table) - postprocessed_table = postprocessed_table.sort_by(self._row_sort_by) - self.assign(self.matching_parts, postprocessed_table.map(lambda row: row[self.PART_NUMBER_COL])) - assert len(postprocessed_table) > 0, "no matching part" # crash to make generator failures more obvious - selected_row = postprocessed_table.first() - self._row_generate(selected_row) + """PartsTablePart that includes the parts selection framework logic. + Subclasses only need to extend _row_filter and _row_generate with part-specific logic.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.part) + + def _row_filter(self, row: PartsTableRow) -> bool: + """Returns whether the candidate row satisfies the requirements (should be kept). + Only called within generate(), so has access to GeneratorParam.get(). + Subclasses should chain this by and-ing with a super() call.""" + return not self.get(self.part) or (self.get(self.part) == row[self.PART_NUMBER_COL]) + + def _table_postprocess(self, table: PartsTable) -> PartsTable: + """Optional postprocessing step that takes a table and returns a transformed table. + Only called within generate(), so has access to GeneratorParam.get(). + Subclasses should chain with the results of a super() call.""" + return table + + @classmethod + def _row_sort_by(cls, row: PartsTableRow) -> Any: + """Defines an optional sorting key for rows of this parts table.""" + return [] + + def _row_generate(self, row: PartsTableRow) -> None: + """Once a row is selected, this is called to generate the implementation given the part selection. + Subclasses super chain this with a super() call. + If there is no matching row, this is not called.""" + self.assign(self.actual_part, row[self.PART_NUMBER_COL]) + + @override + def generate(self) -> None: + matching_table = self._get_table().filter(lambda row: self._row_filter(row)) + postprocessed_table = self._table_postprocess(matching_table) + postprocessed_table = postprocessed_table.sort_by(self._row_sort_by) + self.assign(self.matching_parts, postprocessed_table.map(lambda row: row[self.PART_NUMBER_COL])) + assert len(postprocessed_table) > 0, "no matching part" # crash to make generator failures more obvious + selected_row = postprocessed_table.first() + self._row_generate(selected_row) @abstract_block class SelectorFootprint(PartsTablePart): - """Mixin that allows a specified footprint, for Blocks that automatically select a part.""" - def __init__(self, *args: Any, footprint_spec: StringLike = "", **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.footprint_spec = self.ArgParameter(footprint_spec) # actual_footprint left to the actual footprint + """Mixin that allows a specified footprint, for Blocks that automatically select a part.""" + + def __init__(self, *args: Any, footprint_spec: StringLike = "", **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.footprint_spec = self.ArgParameter(footprint_spec) # actual_footprint left to the actual footprint @non_library class PartsTableFootprintFilter(PartsTableSelector, SelectorFootprint): - """A combination of PartsTableSelector with SelectorFootprint, with row filtering on footprint_spec. - Does not create the footprint itself, this can be used as a base class where footprint filtering is desired - but an internal block is created instead.""" - KICAD_FOOTPRINT = PartsTableColumn(str) + """A combination of PartsTableSelector with SelectorFootprint, with row filtering on footprint_spec. + Does not create the footprint itself, this can be used as a base class where footprint filtering is desired + but an internal block is created instead.""" + + KICAD_FOOTPRINT = PartsTableColumn(str) - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.footprint_spec) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.footprint_spec) - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - ((not self.get(self.footprint_spec)) or self.get(self.footprint_spec) == row[self.KICAD_FOOTPRINT]) + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return super()._row_filter(row) and ( + (not self.get(self.footprint_spec)) or self.get(self.footprint_spec) == row[self.KICAD_FOOTPRINT] + ) @non_library class PartsTableSelectorFootprint(PartsTableFootprintFilter, FootprintBlock, HasStandardFootprint): - """PartsTableFootprintFilter, but also with footprint creation. Must define a standard pinning. - """ - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.footprint( - self._standard_footprint().REFDES_PREFIX, row[self.KICAD_FOOTPRINT], - self._standard_footprint()._make_pinning(self, row[self.KICAD_FOOTPRINT]), - mfr=row[self.MANUFACTURER_COL], part=row[self.PART_NUMBER_COL], - value=row[self.DESCRIPTION_COL], - datasheet=row[self.DATASHEET_COL] - ) + """PartsTableFootprintFilter, but also with footprint creation. Must define a standard pinning.""" + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.footprint( + self._standard_footprint().REFDES_PREFIX, + row[self.KICAD_FOOTPRINT], + self._standard_footprint()._make_pinning(self, row[self.KICAD_FOOTPRINT]), + mfr=row[self.MANUFACTURER_COL], + part=row[self.PART_NUMBER_COL], + value=row[self.DESCRIPTION_COL], + datasheet=row[self.DATASHEET_COL], + ) diff --git a/edg/abstract_parts/PassiveConnector.py b/edg/abstract_parts/PassiveConnector.py index 069cd00e9..6dabf617c 100644 --- a/edg/abstract_parts/PassiveConnector.py +++ b/edg/abstract_parts/PassiveConnector.py @@ -7,59 +7,58 @@ @abstract_block class PassiveConnector(DiscreteComponent, Block): - """A base Block that is an elastic n-ported connector with passive type. - Interface only, no implementation. + """A base Block that is an elastic n-ported connector with passive type. + Interface only, no implementation. - Intended as an infrastructural block where a particular connector series is not fixed, - but can be selected through the refinements system. - An optional length argument can be specified, which forces total number of pins. This must be larger - than the maximum pin index (but can be smaller, unassigned pins are NC). - The allocated pin names correlate with the footprint pin, 1-indexed (per electronics convention). - It is up to the instantiating layer to set the pinmap (or allow the user to set it by refinements).""" - def __init__(self, length: IntLike = 0): - super().__init__() - self.pins = self.Port(Vector(Passive.empty())) - self.actual_length = self.Parameter(IntExpr()) + Intended as an infrastructural block where a particular connector series is not fixed, + but can be selected through the refinements system. + An optional length argument can be specified, which forces total number of pins. This must be larger + than the maximum pin index (but can be smaller, unassigned pins are NC). + The allocated pin names correlate with the footprint pin, 1-indexed (per electronics convention). + It is up to the instantiating layer to set the pinmap (or allow the user to set it by refinements).""" - self.length = self.ArgParameter(length) + def __init__(self, length: IntLike = 0): + super().__init__() + self.pins = self.Port(Vector(Passive.empty())) + self.actual_length = self.Parameter(IntExpr()) + + self.length = self.ArgParameter(length) @abstract_block class FootprintPassiveConnector(PassiveConnector, GeneratorBlock, FootprintBlock): - """PassiveConnector that is a footprint and provides some base functionality for generation.""" - allowed_pins: Iterable[int] + """PassiveConnector that is a footprint and provides some base functionality for generation.""" + + allowed_pins: Iterable[int] - @override - def contents(self) -> None: - super().contents() - self.generator_param(self.length, self.pins.requested()) + @override + def contents(self) -> None: + super().contents() + self.generator_param(self.length, self.pins.requested()) - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - """Returns the part footprint, manufacturer, and name given the number of pins (length). - Implementing classes must implement this method.""" - raise NotImplementedError + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + """Returns the part footprint, manufacturer, and name given the number of pins (length). + Implementing classes must implement this method.""" + raise NotImplementedError - @override - def generate(self) -> None: - super().generate() - max_pin_index = 0 - for pin in self.get(self.pins.requested()): - self.pins.append_elt(Passive(), pin) - assert pin != '0', "cannot have zero pin, explicit pin numbers through suggested_name are required" - max_pin_index = max(max_pin_index, int(pin)) - length = self.get(self.length) - if length == 0: - length = max_pin_index + @override + def generate(self) -> None: + super().generate() + max_pin_index = 0 + for pin in self.get(self.pins.requested()): + self.pins.append_elt(Passive(), pin) + assert pin != "0", "cannot have zero pin, explicit pin numbers through suggested_name are required" + max_pin_index = max(max_pin_index, int(pin)) + length = self.get(self.length) + if length == 0: + length = max_pin_index - self.assign(self.actual_length, length) - self.require(max_pin_index <= self.actual_length, - f"maximum pin index {max_pin_index} over requested length {length}") - # TODO ideally this is require, but we don't support set ops in the IR - assert length in self.allowed_pins, f"requested length {length} outside allowed length {self.allowed_pins}" + self.assign(self.actual_length, length) + self.require( + max_pin_index <= self.actual_length, f"maximum pin index {max_pin_index} over requested length {length}" + ) + # TODO ideally this is require, but we don't support set ops in the IR + assert length in self.allowed_pins, f"requested length {length} outside allowed length {self.allowed_pins}" - (footprint, mfr, part) = self.part_footprint_mfr_name(length) - self.footprint( - 'J', footprint, - {pin_name: pin_port for (pin_name, pin_port) in self.pins.items()}, - mfr, part - ) + (footprint, mfr, part) = self.part_footprint_mfr_name(length) + self.footprint("J", footprint, {pin_name: pin_port for (pin_name, pin_port) in self.pins.items()}, mfr, part) diff --git a/edg/abstract_parts/PassiveFilters.py b/edg/abstract_parts/PassiveFilters.py index fa45e24ba..b905bc97e 100644 --- a/edg/abstract_parts/PassiveFilters.py +++ b/edg/abstract_parts/PassiveFilters.py @@ -10,198 +10,237 @@ class LowPassRc(AnalogFilter, Block): - """Passive-typed low-pass RC specified by the resistor value (impedance) and -3dB (~70%) cutoff frequency.""" - def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike, voltage: RangeLike): - super().__init__() - self.input = self.Port(Passive.empty()) - self.output = self.Port(Passive.empty()) - self.gnd = self.Port(Passive.empty()) - - self.impedance = self.ArgParameter(impedance) - self.cutoff_freq = self.ArgParameter(cutoff_freq) - self.voltage = self.ArgParameter(voltage) - - @override - def contents(self) -> None: - super().contents() - - self.r = self.Block(Resistor(resistance=self.impedance)) # TODO maybe support power? - self.c = self.Block(Capacitor( # cutoff frequency is 1/(2 pi R C) - capacitance=(1 / (2 * pi * self.cutoff_freq)).shrink_multiply(1 / self.impedance), - voltage=self.voltage)) - self.connect(self.input, self.r.a) - self.connect(self.r.b, self.c.pos, self.output) # TODO support negative voltages? - self.connect(self.c.neg, self.gnd) + """Passive-typed low-pass RC specified by the resistor value (impedance) and -3dB (~70%) cutoff frequency.""" + + def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike, voltage: RangeLike): + super().__init__() + self.input = self.Port(Passive.empty()) + self.output = self.Port(Passive.empty()) + self.gnd = self.Port(Passive.empty()) + + self.impedance = self.ArgParameter(impedance) + self.cutoff_freq = self.ArgParameter(cutoff_freq) + self.voltage = self.ArgParameter(voltage) + + @override + def contents(self) -> None: + super().contents() + + self.r = self.Block(Resistor(resistance=self.impedance)) # TODO maybe support power? + self.c = self.Block( + Capacitor( # cutoff frequency is 1/(2 pi R C) + capacitance=(1 / (2 * pi * self.cutoff_freq)).shrink_multiply(1 / self.impedance), voltage=self.voltage + ) + ) + self.connect(self.input, self.r.a) + self.connect(self.r.b, self.c.pos, self.output) # TODO support negative voltages? + self.connect(self.c.neg, self.gnd) class PullupDelayRc(DigitalFilter, Block): - """Pull-up resistor with capacitor for delay. - """ - def __init__(self, impedance: RangeLike, time_constant: RangeLike): - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.time_constant = self.ArgParameter(time_constant) - - self.rc = self.Block(LowPassRc(impedance=impedance, cutoff_freq=1/(2 * pi * self.time_constant), - voltage=self.pwr.link().voltage)) - self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common]) - self.connect(self.pwr, self.rc.input.adapt_to(VoltageSink())) - self.io = self.Export(self.rc.output.adapt_to(DigitalSource.pullup_from_supply(self.pwr)), [Output]) - - def connected(self, *, gnd: Optional[Port[GroundLink]] = None, pwr: Optional[Port[VoltageLink]] = None, - io: Optional[Port[DigitalLink]] = None) -> 'PullupDelayRc': - """Convenience function to connect both ports, returning this object so it can still be given a name.""" - if gnd is not None: - cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) - if pwr is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr, self.pwr) - if io is not None: - cast(Block, builder.get_enclosing_block()).connect(io, self.io) - return self + """Pull-up resistor with capacitor for delay.""" + + def __init__(self, impedance: RangeLike, time_constant: RangeLike): + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.time_constant = self.ArgParameter(time_constant) + + self.rc = self.Block( + LowPassRc( + impedance=impedance, cutoff_freq=1 / (2 * pi * self.time_constant), voltage=self.pwr.link().voltage + ) + ) + self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common]) + self.connect(self.pwr, self.rc.input.adapt_to(VoltageSink())) + self.io = self.Export(self.rc.output.adapt_to(DigitalSource.pullup_from_supply(self.pwr)), [Output]) + + def connected( + self, + *, + gnd: Optional[Port[GroundLink]] = None, + pwr: Optional[Port[VoltageLink]] = None, + io: Optional[Port[DigitalLink]] = None, + ) -> "PullupDelayRc": + """Convenience function to connect both ports, returning this object so it can still be given a name.""" + if gnd is not None: + cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) + if pwr is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr, self.pwr) + if io is not None: + cast(Block, builder.get_enclosing_block()).connect(io, self.io) + return self class AnalogLowPassRc(DigitalFilter, Block): - """Low-pass RC filter attached to an analog line. - """ - def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): - super().__init__() - self.input = self.Port(AnalogSink.empty(), [Input]) - self.output = self.Port(AnalogSource.empty(), [Output]) - - self.rc = self.Block(LowPassRc(impedance=impedance, cutoff_freq=cutoff_freq, - voltage=self.input.link().voltage)) - self.connect(self.input, self.rc.input.adapt_to(AnalogSink( - current_draw=self.output.link().current_drawn - ))) - self.connect(self.output, self.rc.output.adapt_to(AnalogSource( - voltage_out=self.input.link().voltage, - signal_out=self.input.link().signal - ))) - - self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common]) + """Low-pass RC filter attached to an analog line.""" + + def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): + super().__init__() + self.input = self.Port(AnalogSink.empty(), [Input]) + self.output = self.Port(AnalogSource.empty(), [Output]) + + self.rc = self.Block(LowPassRc(impedance=impedance, cutoff_freq=cutoff_freq, voltage=self.input.link().voltage)) + self.connect(self.input, self.rc.input.adapt_to(AnalogSink(current_draw=self.output.link().current_drawn))) + self.connect( + self.output, + self.rc.output.adapt_to( + AnalogSource(voltage_out=self.input.link().voltage, signal_out=self.input.link().signal) + ), + ) + + self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common]) class DigitalLowPassRc(DigitalFilter, Block): - """Low-pass RC filter attached to a digital line. - Does not change the signal, only performs filtering - """ - def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): - super().__init__() - self.input = self.Port(DigitalSink.empty(), [Input]) - self.output = self.Port(DigitalSource.empty(), [Output]) - - self.rc = self.Block(LowPassRc(impedance=impedance, cutoff_freq=cutoff_freq, - voltage=self.input.link().voltage)) - self.connect(self.input, self.rc.input.adapt_to(DigitalSink( - current_draw=self.output.link().current_drawn - ))) - self.connect(self.output, self.rc.output.adapt_to(DigitalSource( - current_limits=RangeExpr.ALL, - voltage_out=self.input.link().voltage, - output_thresholds=self.input.link().output_thresholds - ))) - - self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common]) + """Low-pass RC filter attached to a digital line. + Does not change the signal, only performs filtering + """ + + def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): + super().__init__() + self.input = self.Port(DigitalSink.empty(), [Input]) + self.output = self.Port(DigitalSource.empty(), [Output]) + + self.rc = self.Block(LowPassRc(impedance=impedance, cutoff_freq=cutoff_freq, voltage=self.input.link().voltage)) + self.connect(self.input, self.rc.input.adapt_to(DigitalSink(current_draw=self.output.link().current_drawn))) + self.connect( + self.output, + self.rc.output.adapt_to( + DigitalSource( + current_limits=RangeExpr.ALL, + voltage_out=self.input.link().voltage, + output_thresholds=self.input.link().output_thresholds, + ) + ), + ) + + self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common]) class DigitalLowPassRcArray(DigitalFilter, GeneratorBlock): - """Array of DigitalLowPassRc, currently takes its size from the output. - TODO: properly size when either input or output is sized? - """ - def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): - super().__init__() - self.input = self.Port(Vector(DigitalSink.empty()), [Input]) - self.output = self.Port(Vector(DigitalSource.empty()), [Output]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.impedance = self.ArgParameter(impedance) - self.cutoff_freq = self.ArgParameter(cutoff_freq) - - self.generator_param(self.output.requested()) - - @override - def generate(self) -> None: - super().generate() - self.elts = ElementDict[DigitalLowPassRc]() - model = DigitalLowPassRc(self.impedance, self.cutoff_freq) - for requested in self.get(self.output.requested()): - self.elts[requested] = elt = self.Block(model) - self.connect(self.input.append_elt(DigitalSink.empty(), requested), elt.input) - self.connect(self.output.append_elt(DigitalSource.empty(), requested), elt.output) - self.connect(self.gnd, elt.gnd) + """Array of DigitalLowPassRc, currently takes its size from the output. + TODO: properly size when either input or output is sized? + """ + + def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): + super().__init__() + self.input = self.Port(Vector(DigitalSink.empty()), [Input]) + self.output = self.Port(Vector(DigitalSource.empty()), [Output]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.impedance = self.ArgParameter(impedance) + self.cutoff_freq = self.ArgParameter(cutoff_freq) + + self.generator_param(self.output.requested()) + + @override + def generate(self) -> None: + super().generate() + self.elts = ElementDict[DigitalLowPassRc]() + model = DigitalLowPassRc(self.impedance, self.cutoff_freq) + for requested in self.get(self.output.requested()): + self.elts[requested] = elt = self.Block(model) + self.connect(self.input.append_elt(DigitalSink.empty(), requested), elt.input) + self.connect(self.output.append_elt(DigitalSource.empty(), requested), elt.output) + self.connect(self.gnd, elt.gnd) class LowPassRcDac(DigitalToAnalog, Block): - """Low-pass RC filter used as a simple DAC by filtering out a PWM signal. - The cutoff frequency of the filter should be sufficiently beneath the PWM frequency, - but enough above baseband to not distort the signal. - Lower frequencies will result in either higher impedance or larger caps. - This must be manually specified, since PWM frequency data is not part of the electronics model. - """ - def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): - super().__init__() - self.input = self.Port(DigitalSink.empty(), [Input]) - self.output = self.Port(AnalogSource.empty(), [Output]) - - self.rc = self.Block(LowPassRc(impedance=impedance, cutoff_freq=cutoff_freq, - voltage=self.input.link().voltage)) - self.connect(self.input, self.rc.input.adapt_to(DigitalSink( - current_draw=self.output.link().current_drawn - ))) - self.connect(self.output, self.rc.output.adapt_to(AnalogSource( - voltage_out=self.input.link().voltage, - signal_out=self.input.link().voltage, - impedance=impedance # TODO use selected resistance from RC filter - ))) - - self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common]) + """Low-pass RC filter used as a simple DAC by filtering out a PWM signal. + The cutoff frequency of the filter should be sufficiently beneath the PWM frequency, + but enough above baseband to not distort the signal. + Lower frequencies will result in either higher impedance or larger caps. + This must be manually specified, since PWM frequency data is not part of the electronics model. + """ + + def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): + super().__init__() + self.input = self.Port(DigitalSink.empty(), [Input]) + self.output = self.Port(AnalogSource.empty(), [Output]) + + self.rc = self.Block(LowPassRc(impedance=impedance, cutoff_freq=cutoff_freq, voltage=self.input.link().voltage)) + self.connect(self.input, self.rc.input.adapt_to(DigitalSink(current_draw=self.output.link().current_drawn))) + self.connect( + self.output, + self.rc.output.adapt_to( + AnalogSource( + voltage_out=self.input.link().voltage, + signal_out=self.input.link().voltage, + impedance=impedance, # TODO use selected resistance from RC filter + ) + ), + ) + + self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common]) class LowPassAnalogDifferentialRc(AnalogFilter, KiCadImportableBlock): - """Analog-typed low-pass differential RC filter, with cutoff frequency specified at the -3dB (~70%) point. - Impedance is the single-ended resistor value.""" - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name == 'edg_importable:DifferentialRC' - return { - '1': self.inp, '2': self.inn, '3': self.outp, '4': self.outn - } - - def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): - super().__init__() - self.inn = self.Port(AnalogSink.empty()) - self.inp = self.Port(AnalogSink.empty()) - self.outn = self.Port(AnalogSource.empty()) - self.outp = self.Port(AnalogSource.empty()) - - self.impedance = self.ArgParameter(impedance) - self.cutoff_freq = self.ArgParameter(cutoff_freq) - - @override - def contents(self) -> None: - super().contents() - - self.rp = self.Block(Resistor(resistance=self.impedance)) - self.rn = self.Block(Resistor(resistance=self.impedance)) - capacitance = (1 / self.cutoff_freq).shrink_multiply(1 / (2 * pi * self.impedance)) - # capacitance is single-ended, halve it for differential - self.c = self.Block(Capacitor(capacitance=0.5*capacitance, - voltage=self.inp.link().voltage-self.inn.link().voltage)) - self.connect(self.inp, self.rp.a.adapt_to(AnalogSink( - impedance=self.rp.actual_resistance + self.outp.link().sink_impedance, - current_draw=self.outp.link().current_drawn - ))) - self.connect(self.inn, self.rn.a.adapt_to(AnalogSink( - impedance=self.rn.actual_resistance + self.outn.link().sink_impedance, - current_draw=self.outn.link().current_drawn - ))) - self.connect(self.outp, self.rp.b.adapt_to(AnalogSource( - voltage_out=self.inp.link().voltage, - signal_out=self.inp.link().signal, - impedance=self.rp.actual_resistance + self.inp.link().source_impedance - )), self.c.pos.adapt_to(AnalogSink())) - self.connect(self.outn, self.rn.b.adapt_to(AnalogSource( - voltage_out=self.inn.link().voltage, - signal_out=self.inn.link().signal, - impedance=self.rn.actual_resistance + self.inn.link().source_impedance - )), self.c.neg.adapt_to(AnalogSink())) + """Analog-typed low-pass differential RC filter, with cutoff frequency specified at the -3dB (~70%) point. + Impedance is the single-ended resistor value.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name == "edg_importable:DifferentialRC" + return {"1": self.inp, "2": self.inn, "3": self.outp, "4": self.outn} + + def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike): + super().__init__() + self.inn = self.Port(AnalogSink.empty()) + self.inp = self.Port(AnalogSink.empty()) + self.outn = self.Port(AnalogSource.empty()) + self.outp = self.Port(AnalogSource.empty()) + + self.impedance = self.ArgParameter(impedance) + self.cutoff_freq = self.ArgParameter(cutoff_freq) + + @override + def contents(self) -> None: + super().contents() + + self.rp = self.Block(Resistor(resistance=self.impedance)) + self.rn = self.Block(Resistor(resistance=self.impedance)) + capacitance = (1 / self.cutoff_freq).shrink_multiply(1 / (2 * pi * self.impedance)) + # capacitance is single-ended, halve it for differential + self.c = self.Block( + Capacitor(capacitance=0.5 * capacitance, voltage=self.inp.link().voltage - self.inn.link().voltage) + ) + self.connect( + self.inp, + self.rp.a.adapt_to( + AnalogSink( + impedance=self.rp.actual_resistance + self.outp.link().sink_impedance, + current_draw=self.outp.link().current_drawn, + ) + ), + ) + self.connect( + self.inn, + self.rn.a.adapt_to( + AnalogSink( + impedance=self.rn.actual_resistance + self.outn.link().sink_impedance, + current_draw=self.outn.link().current_drawn, + ) + ), + ) + self.connect( + self.outp, + self.rp.b.adapt_to( + AnalogSource( + voltage_out=self.inp.link().voltage, + signal_out=self.inp.link().signal, + impedance=self.rp.actual_resistance + self.inp.link().source_impedance, + ) + ), + self.c.pos.adapt_to(AnalogSink()), + ) + self.connect( + self.outn, + self.rn.b.adapt_to( + AnalogSource( + voltage_out=self.inn.link().voltage, + signal_out=self.inn.link().signal, + impedance=self.rn.actual_resistance + self.inn.link().source_impedance, + ) + ), + self.c.neg.adapt_to(AnalogSink()), + ) diff --git a/edg/abstract_parts/PinMappable.py b/edg/abstract_parts/PinMappable.py index 9a0b83e1c..9fba3cf54 100644 --- a/edg/abstract_parts/PinMappable.py +++ b/edg/abstract_parts/PinMappable.py @@ -8,145 +8,163 @@ @non_library class PinMappable(Block): - """Abstract base class for pin-mappable devices. Provides a named initializer argument for user-defined explicit - pin mapping refinements. Actual pin mapping functionality must be implemented by the subclass. - This may simply delegate the pin mapping to an inner block, for example for a microcontroller application circuit - to delegate the pin mapping to the microcontroller chip block. - """ - def __init__(self, pin_assigns: ArrayStringLike = []) -> None: - super().__init__() - self.pin_assigns = self.ArgParameter(pin_assigns) - self.actual_pin_assigns = self.Parameter(ArrayStringExpr()) - - def generator_set_allocation(self, allocations: List['AllocatedResource']) -> None: - allocation_strs = [] - for allocation in allocations: - if allocation.pin is None: - allocation_strs.append(f"{allocation.name}={allocation.resource_name}") - elif isinstance(allocation.pin, str): - allocation_strs.append(f"{allocation.name}={allocation.resource_name}, {allocation.pin}") - elif isinstance(allocation.pin, dict): - allocation_strs.append(f"{allocation.name}={allocation.resource_name}") - for subport_name, (subport_pin, subport_resource) in allocation.pin.items(): - if subport_resource is not None: - allocation_strs.append(f"{allocation.name}.{subport_name}={subport_resource}, {subport_pin}") - else: - allocation_strs.append(f"{allocation.name}.{subport_name}={subport_pin}") - else: - raise ValueError(f"unknown allocation type {allocation}") - - self.assign(self.actual_pin_assigns, allocation_strs) + """Abstract base class for pin-mappable devices. Provides a named initializer argument for user-defined explicit + pin mapping refinements. Actual pin mapping functionality must be implemented by the subclass. + This may simply delegate the pin mapping to an inner block, for example for a microcontroller application circuit + to delegate the pin mapping to the microcontroller chip block. + """ + + def __init__(self, pin_assigns: ArrayStringLike = []) -> None: + super().__init__() + self.pin_assigns = self.ArgParameter(pin_assigns) + self.actual_pin_assigns = self.Parameter(ArrayStringExpr()) + + def generator_set_allocation(self, allocations: List["AllocatedResource"]) -> None: + allocation_strs = [] + for allocation in allocations: + if allocation.pin is None: + allocation_strs.append(f"{allocation.name}={allocation.resource_name}") + elif isinstance(allocation.pin, str): + allocation_strs.append(f"{allocation.name}={allocation.resource_name}, {allocation.pin}") + elif isinstance(allocation.pin, dict): + allocation_strs.append(f"{allocation.name}={allocation.resource_name}") + for subport_name, (subport_pin, subport_resource) in allocation.pin.items(): + if subport_resource is not None: + allocation_strs.append(f"{allocation.name}.{subport_name}={subport_resource}, {subport_pin}") + else: + allocation_strs.append(f"{allocation.name}.{subport_name}={subport_pin}") + else: + raise ValueError(f"unknown allocation type {allocation}") + + self.assign(self.actual_pin_assigns, allocation_strs) class BasePinMapResource: - """Abstract base class for a resource definition for the pin mapping utility. Because these are so intertwined with - the actual pin mapper, these are just named data structures - the logic for handling these is in the pin mapper.""" + """Abstract base class for a resource definition for the pin mapping utility. Because these are so intertwined with + the actual pin mapper, these are just named data structures - the logic for handling these is in the pin mapper.""" class BaseLeafPinMapResource(BasePinMapResource): - """Abstract base class for a resource that does not delegate any further (leaf-level) - such as a pin.""" + """Abstract base class for a resource that does not delegate any further (leaf-level) - such as a pin.""" class BaseDelegatingPinMapResource(BasePinMapResource): - """Abstract base class for a resource that delegates inner resources.""" + """Abstract base class for a resource that delegates inner resources.""" class PinResource(BaseLeafPinMapResource): - """A resource for a single chip pin, which can be one of several port types (eg, an ADC and DIO sharing a pin).""" - def __init__(self, pin: str, name_models: Dict[str, CircuitPort]): - self.pin = pin - self.name_models = name_models + """A resource for a single chip pin, which can be one of several port types (eg, an ADC and DIO sharing a pin).""" - @override - def __repr__(self) -> str: - return f"PinResource({self.pin}, {self.name_models})" + def __init__(self, pin: str, name_models: Dict[str, CircuitPort]): + self.pin = pin + self.name_models = name_models - @override - def __eq__(self, other: Any) -> bool: - # TODO avoid using is if we can compare port model equality - return isinstance(other, PinResource) and self.pin == other.pin and self.name_models is other.name_models + @override + def __repr__(self) -> str: + return f"PinResource({self.pin}, {self.name_models})" - def get_name_model_for_type(self, tpe: Type[Port]) -> Tuple[str, Port]: - for (name, model) in self.name_models.items(): - if isinstance(model, tpe): - return (name, model) - raise ValueError(f"no name/models of type {tpe} in PinResource with {self.name_models}") + @override + def __eq__(self, other: Any) -> bool: + # TODO avoid using is if we can compare port model equality + return isinstance(other, PinResource) and self.pin == other.pin and self.name_models is other.name_models + + def get_name_model_for_type(self, tpe: Type[Port]) -> Tuple[str, Port]: + for name, model in self.name_models.items(): + if isinstance(model, tpe): + return (name, model) + raise ValueError(f"no name/models of type {tpe} in PinResource with {self.name_models}") class PeripheralFixedPin(BaseLeafPinMapResource): - """A resource for a peripheral as a Bundle port, where the internal ports are fixed. No allocation happens. - The internal port model must be fully defined here.""" - def __init__(self, name: str, port_model: Bundle, inner_allowed_pins: Dict[str, str]): - self.name = name - self.port_model = port_model - self.inner_allowed_pins = inner_allowed_pins + """A resource for a peripheral as a Bundle port, where the internal ports are fixed. No allocation happens. + The internal port model must be fully defined here.""" + + def __init__(self, name: str, port_model: Bundle, inner_allowed_pins: Dict[str, str]): + self.name = name + self.port_model = port_model + self.inner_allowed_pins = inner_allowed_pins - @override - def __repr__(self) -> str: - return f"PeripheralFixedPin({self.name}, {self.port_model.__class__.__name__} {self.inner_allowed_pins})" + @override + def __repr__(self) -> str: + return f"PeripheralFixedPin({self.name}, {self.port_model.__class__.__name__} {self.inner_allowed_pins})" - @override - def __eq__(self, other: Any) -> bool: - # TODO avoid using is if we can compare port model equality - return isinstance(other, PeripheralFixedPin) and self.name == other.name and \ - self.port_model is other.port_model and self.inner_allowed_pins == other.inner_allowed_pins + @override + def __eq__(self, other: Any) -> bool: + # TODO avoid using is if we can compare port model equality + return ( + isinstance(other, PeripheralFixedPin) + and self.name == other.name + and self.port_model is other.port_model + and self.inner_allowed_pins == other.inner_allowed_pins + ) class PeripheralAnyResource(BaseDelegatingPinMapResource): - """A resource for a peripheral as a Bundle port, where the internal ports must be delegated to another resource, - any resource of matching type. Used for chips with a full switch matrix. - The port model here should have empty models for the internal ports, so the models can be assigned from the inner - resource. This allows things like digital IOs in a peripheral to inherit from the pin-level definition.""" - def __init__(self, name: str, port_model: Bundle): - self.name = name - self.port_model = port_model + """A resource for a peripheral as a Bundle port, where the internal ports must be delegated to another resource, + any resource of matching type. Used for chips with a full switch matrix. + The port model here should have empty models for the internal ports, so the models can be assigned from the inner + resource. This allows things like digital IOs in a peripheral to inherit from the pin-level definition.""" + + def __init__(self, name: str, port_model: Bundle): + self.name = name + self.port_model = port_model - @override - def __repr__(self) -> str: - return f"PeripheralAnyResource({self.name}, {self.port_model.__class__.__name__})" + @override + def __repr__(self) -> str: + return f"PeripheralAnyResource({self.name}, {self.port_model.__class__.__name__})" - @override - def __eq__(self, other: Any) -> bool: - # TODO avoid using is if we can compare port model equality - return isinstance(other, PeripheralAnyResource) and self.name == other.name and \ - self.port_model is other.port_model + @override + def __eq__(self, other: Any) -> bool: + # TODO avoid using is if we can compare port model equality + return ( + isinstance(other, PeripheralAnyResource) and self.name == other.name and self.port_model is other.port_model + ) class PeripheralFixedResource(BaseDelegatingPinMapResource): - """A resource for a peripheral as a Bundle port, where the internal ports must be delegated to another resource, - of a fixed list per pin by resource name. Used for chips which have alternate pin functionality - (sort of a very limited switch matrix). - The port model here should have empty models for the internal ports, so the models can be assigned from the inner - resource. This allows things like digital IOs in a peripheral to inherit from the pin-level definition.""" - def __init__(self, name: str, port_model: Bundle, inner_allowed_names: Dict[str, List[str]]): - self.name = name - self.port_model = port_model - self.inner_allowed_names = inner_allowed_names - - @override - def __repr__(self) -> str: - return f"PeripheralFixedResource({self.name}, {self.port_model.__class__.__name__}, {self.inner_allowed_names})" - - @override - def __eq__(self, other: Any) -> bool: - # TODO avoid using is if we can compare port model equality - return isinstance(other, PeripheralFixedResource) and self.name == other.name and \ - self.port_model is other.port_model and self.inner_allowed_names == other.inner_allowed_names + """A resource for a peripheral as a Bundle port, where the internal ports must be delegated to another resource, + of a fixed list per pin by resource name. Used for chips which have alternate pin functionality + (sort of a very limited switch matrix). + The port model here should have empty models for the internal ports, so the models can be assigned from the inner + resource. This allows things like digital IOs in a peripheral to inherit from the pin-level definition.""" + + def __init__(self, name: str, port_model: Bundle, inner_allowed_names: Dict[str, List[str]]): + self.name = name + self.port_model = port_model + self.inner_allowed_names = inner_allowed_names + + @override + def __repr__(self) -> str: + return f"PeripheralFixedResource({self.name}, {self.port_model.__class__.__name__}, {self.inner_allowed_names})" + + @override + def __eq__(self, other: Any) -> bool: + # TODO avoid using is if we can compare port model equality + return ( + isinstance(other, PeripheralFixedResource) + and self.name == other.name + and self.port_model is other.port_model + and self.inner_allowed_names == other.inner_allowed_names + ) class AllocatedResource(NamedTuple): - port_model: Port # port model (including defined elements, for bundles) - name: str # name given by the user, bundles will have automatic postfixes - resource_name: str # name of the resource assigned, non-delegated bundle elements can have automatic prefixes - # TODO recursive definition for bundles with Dict[str, AllocatedResource]? but mypy doesn't like recursive types - pin: Union[str, None, Dict[str, Tuple[str, Optional[str]]]] # pin number if port is leaf, or - # recursive definition for bundles (pin, resource) - - @override - def __eq__(self, other: Any) -> bool: - # TODO better port model check, perhaps by initializer - return self.port_model is other.port_model and self.name == other.name and \ - self.resource_name is other.resource_name and self.pin == other.pin + port_model: Port # port model (including defined elements, for bundles) + name: str # name given by the user, bundles will have automatic postfixes + resource_name: str # name of the resource assigned, non-delegated bundle elements can have automatic prefixes + # TODO recursive definition for bundles with Dict[str, AllocatedResource]? but mypy doesn't like recursive types + pin: Union[str, None, Dict[str, Tuple[str, Optional[str]]]] # pin number if port is leaf, or + # recursive definition for bundles (pin, resource) + + @override + def __eq__(self, other: Any) -> bool: + # TODO better port model check, perhaps by initializer + return ( + self.port_model is other.port_model + and self.name == other.name + and self.resource_name is other.resource_name + and self.pin == other.pin + ) # Defines a way to convert port models into related types for use in bundles, for example from the @@ -154,271 +172,297 @@ def __eq__(self, other: Any) -> bool: # Specified as entries of target port type: (source port type, conversion function) PortTransformsType = Mapping[Type[Port], Tuple[Type[Port], Callable[[Port], Port]]] DefaultPortTransforms: PortTransformsType = { - DigitalSource: (DigitalBidir, DigitalSource.from_bidir), # type: ignore - DigitalSink: (DigitalBidir, DigitalSink.from_bidir), # type: ignore + DigitalSource: (DigitalBidir, DigitalSource.from_bidir), # type: ignore + DigitalSink: (DigitalBidir, DigitalSink.from_bidir), # type: ignore } class BadUserAssignError(RuntimeError): - """Indicates an error with an user-specified assignment.""" - pass + """Indicates an error with an user-specified assignment.""" + pass -class AutomaticAllocationError(RuntimeError): - """If automatic assignment was unable to complete, for example if there were more assigns than resources. - Not a fundamental error, could simply be because the simplistic assignment algorithm wasn't able to complete.""" - pass +class AutomaticAllocationError(RuntimeError): + """If automatic assignment was unable to complete, for example if there were more assigns than resources. + Not a fundamental error, could simply be because the simplistic assignment algorithm wasn't able to complete.""" -class UserAssignmentDict: - """A recursive dict mapping for user assignment strings, structured as a dict keyed by the top-level prefix of the - assignment name. Contains a root (that is string-valued) and recursive-valued elements.""" - @staticmethod - def from_spec(assignments_spec: List[str]) -> 'UserAssignmentDict': - def parse_elt(assignment_spec: str) -> Tuple[List[str], str]: - assignment_split = assignment_spec.split('=') - assert len(assignment_split) == 2, f"bad assignment spec {assignment_spec}" - return (assignment_split[0].split('.'), assignment_split[1]) - assignment_spec_list = [parse_elt(assignment) for assignment in assignments_spec] - root, assignments = UserAssignmentDict._from_list(assignment_spec_list, []) - if root is not None: - raise BadUserAssignError(f"unexpected root assignment to {root}") - return assignments - - @staticmethod - def _from_list(assignment_spec_list: List[Tuple[List[str], str]], name: List[str]) -> \ - Tuple[Optional[str], 'UserAssignmentDict']: - """Given a list of parsed assignment specs [([named path], resource/pin)], extracts the first component of the - path as the dict key, and remaps each entry to only contain the path postfix. - If the path is empty, it gets mapped to the empty-string key, preserving the empty path.""" - root_assigns: Set[str] = set() - dict_out: Dict[str, List[Tuple[List[str], str]]] = {} - for (named_path, pin) in assignment_spec_list: - if not named_path: - root_assigns.add(pin) - else: - dict_out.setdefault(named_path[0], []).append((named_path[1:], pin)) - if len(root_assigns) >= 1: - if len(root_assigns) > 1: - raise BadUserAssignError(f"multiple assigns to {'.'.join(name)}: {','.join(root_assigns)}") - root_assign: Optional[str] = root_assigns.pop() - else: - root_assign = None - return (root_assign, UserAssignmentDict(dict_out, name)) - - def __init__(self, elts: Dict[str, List[Tuple[List[str], str]]], name: List[str]): - self.elts = elts - self.name = name - self.marked: Set[str] = set() - - def contains_elt(self, elt: str) -> bool: - """Returns whether an element is still in the dict, without marking the element as read.""" - return elt in self.elts - - def get_elt(self, elt: str) -> Tuple[Optional[str], 'UserAssignmentDict']: - """Returns the top-level assignment for an element (or None, if no entry), plus the recursive UserAssignDict. - Marks the element as read.""" - if elt in self.elts: - self.marked.add(elt) - return self._from_list(self.elts[elt], self.name + [elt]) - else: - return (None, UserAssignmentDict({}, self.name + [elt])) - - def check_empty(self) -> None: - """Checks that all assignments have been processed, otherwise raises a BadUserAssignError.""" - unprocessed = set(self.elts.keys()).difference(self.marked) - if unprocessed: - raise BadUserAssignError(f"unprocessed assignments in {'.'.join(self.name)}: {unprocessed}") + pass -class PinMapUtil: - """Pin mapping utility, that takes in a definition of resources (pins and peripherals on a chip) and assigns them - automatically, optionally with user-defined explicit assignments.""" - def __init__(self, resources: List[BasePinMapResource], transforms: Optional[PortTransformsType] = None): - self.resources = resources - self.transforms = DefaultPortTransforms if transforms is None else transforms - - def add(self, additional_resources: List[BasePinMapResource]) -> 'PinMapUtil': - """Returns a new PinMapUtil with additional resources.""" - return PinMapUtil(self.resources + additional_resources, self.transforms) - - def remap_pins(self, pinmap: Dict[str, str]) -> 'PinMapUtil': - """Returns a new PinMapUtil with pin names remapped according to the argument dict. Useful for a chip series - to define a generic pin mapping using pin names, and then remap those to pin numbers for specific packages. - Pins that are not in the pinmap are discarded.""" - def remap_resource(resource: BasePinMapResource) -> Optional[BasePinMapResource]: - if isinstance(resource, PinResource): - if resource.pin in pinmap: - return PinResource(pinmap[resource.pin], resource.name_models) +class UserAssignmentDict: + """A recursive dict mapping for user assignment strings, structured as a dict keyed by the top-level prefix of the + assignment name. Contains a root (that is string-valued) and recursive-valued elements.""" + + @staticmethod + def from_spec(assignments_spec: List[str]) -> "UserAssignmentDict": + def parse_elt(assignment_spec: str) -> Tuple[List[str], str]: + assignment_split = assignment_spec.split("=") + assert len(assignment_split) == 2, f"bad assignment spec {assignment_spec}" + return (assignment_split[0].split("."), assignment_split[1]) + + assignment_spec_list = [parse_elt(assignment) for assignment in assignments_spec] + root, assignments = UserAssignmentDict._from_list(assignment_spec_list, []) + if root is not None: + raise BadUserAssignError(f"unexpected root assignment to {root}") + return assignments + + @staticmethod + def _from_list( + assignment_spec_list: List[Tuple[List[str], str]], name: List[str] + ) -> Tuple[Optional[str], "UserAssignmentDict"]: + """Given a list of parsed assignment specs [([named path], resource/pin)], extracts the first component of the + path as the dict key, and remaps each entry to only contain the path postfix. + If the path is empty, it gets mapped to the empty-string key, preserving the empty path.""" + root_assigns: Set[str] = set() + dict_out: Dict[str, List[Tuple[List[str], str]]] = {} + for named_path, pin in assignment_spec_list: + if not named_path: + root_assigns.add(pin) + else: + dict_out.setdefault(named_path[0], []).append((named_path[1:], pin)) + if len(root_assigns) >= 1: + if len(root_assigns) > 1: + raise BadUserAssignError(f"multiple assigns to {'.'.join(name)}: {','.join(root_assigns)}") + root_assign: Optional[str] = root_assigns.pop() else: - return None - elif isinstance(resource, PeripheralFixedPin): - remapped_pins = {elt_name: pinmap[elt_pin] for elt_name, elt_pin in resource.inner_allowed_pins.items() - if elt_pin in pinmap} - return PeripheralFixedPin(resource.name, resource.port_model, remapped_pins) - elif isinstance(resource, BaseDelegatingPinMapResource): - return resource - else: - raise NotImplementedError(f"unknown resource {resource}") - - remapped_resources_raw = [remap_resource(resource) for resource in self.resources] - remapped_resources = [resource for resource in remapped_resources_raw if resource is not None] - - def resource_pin(resource: BasePinMapResource) -> List[str]: - if isinstance(resource, PinResource): - return [resource.pin] - elif isinstance(resource, PeripheralFixedPin): - return list(resource.inner_allowed_pins.values()) - elif isinstance(resource, BaseDelegatingPinMapResource): - return [] - else: - raise NotImplementedError(f"unknown resource {resource}") - all_pins = itertools.chain(*[resource_pin(resource) for resource in self.resources]) - missed_names = set(pinmap.keys()).difference(all_pins) - assert not missed_names, f"invalid pins in remap: {missed_names}" - - return PinMapUtil(remapped_resources, self.transforms) - - @staticmethod - def _resource_port_types(resource: BasePinMapResource) -> List[Type[Port]]: - if isinstance(resource, PinResource): - return [type(model) for resource_name, model in resource.name_models.items()] - elif isinstance(resource, (PeripheralFixedPin, PeripheralAnyResource, PeripheralFixedResource)): - return [type(resource.port_model)] - else: - raise NotImplementedError(f"unsupported resource type {resource}") - - @staticmethod - def _resource_names(resource: BasePinMapResource) -> List[str]: - if isinstance(resource, PinResource): - return [resource.pin] + [resource_name for resource_name, model in resource.name_models.items()] - elif isinstance(resource, (PeripheralFixedPin, PeripheralAnyResource, PeripheralFixedResource)): - return [resource.name] - else: - raise NotImplementedError(f"unsupported resource type {resource}") - - def allocate(self, port_types_names: List[Tuple[Type[Port], List[str]]], assignments_spec: List[str] = []) -> \ - List[AllocatedResource]: - """Performs port assignment given a list of port types and their names, and optional user-defined pin assignments - (which may be empty). Names may be duplicated (either within a port type, or across port types), and multiple - records will show up accordingly in the output data structure. - Returns a list of assigned ports, structured as a port model (set recursively for bundle ports), the input name, - and pin assignment as a pin string for leaf ports, or a dict (possibly recursive) for bundles.""" - assignments = UserAssignmentDict.from_spec(assignments_spec) - - # mutable data structure, resources will be removed as they are assigned - free_resources_by_type: Dict[Type[Port], List[BasePinMapResource]] = {} - for resource in self.resources: - for supported_type in self._resource_port_types(resource): - free_resources_by_type.setdefault(supported_type, []).append(resource) - - def mark_resource_used(resource: BasePinMapResource) -> None: - for supported_type in self._resource_port_types(resource): - free_resources_by_type[supported_type].remove(resource) - - # unlike the above, resources are not deleted from this - resources_by_name: Dict[str, List[BasePinMapResource]] = {} - for resource in self.resources: - for resource_name in self._resource_names(resource): - resources_by_name.setdefault(resource_name, []).append(resource) - - allocated_resources: List[AllocatedResource] = [] - - def try_allocate_resource(port_type: Type[Port], port_name: str, resource: BasePinMapResource, - sub_assignments: UserAssignmentDict) -> AllocatedResource: - """Try to assign a port to the specified resource, returning any resources used by this mapping and - the pin mapping. - Backtracking search is not implemented, this cannot fail.""" - if isinstance(resource, PinResource): # single pin: just assign it - sub_assignments.check_empty() - resource_name, resource_model = resource.get_name_model_for_type(port_type) - allocated_resource = AllocatedResource(resource_model, port_name, resource_name, resource.pin) - return allocated_resource - elif isinstance(resource, PeripheralFixedPin): # fixed pin: check user-assignment, or assign first - inner_pin_map: Dict[str, Tuple[str, Optional[str]]] = {} - for (inner_name, inner_pin) in resource.inner_allowed_pins.items(): # TODO should this be recursive? - inner_assignment, inner_sub_assignments = sub_assignments.get_elt(inner_name) - if inner_assignment is not None and inner_assignment != inner_pin: - raise BadUserAssignError(f"invalid assignment to {port_name}.{inner_name}: {inner_assignment}") - - inner_pin_map[inner_name] = (inner_pin, None) - inner_sub_assignments.check_empty() - - sub_assignments.check_empty() - allocated_resource = AllocatedResource(resource.port_model, port_name, resource.name, inner_pin_map) - return allocated_resource - elif isinstance(resource, (PeripheralAnyResource, PeripheralFixedResource)): # any-pin: delegate allocation - inner_pin_map = {} - resource_model = resource.port_model # typer gets confused if this is put directly where it is used - inner_models = {} - resource_name = resource.name # typer gets confused if this is put directly where it is used - for (inner_name, inner_model) in resource.port_model._ports.items(): - inner_type = type(inner_model) - if inner_type in self.transforms: # apply transform to search for the resource type, if needed - inner_type = self.transforms[inner_type][0] - - inner_assignment, inner_sub_assignments = sub_assignments.get_elt(inner_name) - - resource_pool = free_resources_by_type[inner_type] - if isinstance(resource, PeripheralFixedResource): # filter further by allowed pins for this peripheral - allowed_names = resource.inner_allowed_names.get(inner_name, []) - allowed_resources = list(itertools.chain(*[resources_by_name.get(name, []) - for name in allowed_names])) - resource_pool = [resource for resource in resource_pool if resource in allowed_resources] - - inner_allocation = allocate_port_type(resource_pool, inner_type, f'{port_name}.{inner_name}', - inner_assignment, inner_sub_assignments) - if inner_allocation.pin is not None: - assert isinstance(inner_allocation.pin, str) - inner_pin_map[inner_name] = (inner_allocation.pin, inner_allocation.resource_name) - - inner_models[inner_name] = inner_allocation.port_model - if type(inner_model) in self.transforms: # apply transform to search for the resource type, if needed - inner_models[inner_name] = self.transforms[type(inner_model)][1](inner_models[inner_name]) - sub_assignments.check_empty() - resource_model = resource_model.with_elt_initializers(inner_models) - allocated_resource = AllocatedResource(resource_model, port_name, resource_name, inner_pin_map) - return allocated_resource - else: - raise NotImplementedError(f"unsupported resource type {resource}") - - # allocates a resource greedily (or errors out), and consumes the relevant resource - def allocate_port_type(resource_pool: List[BasePinMapResource], port_type: Type[Port], port_name: str, - assignment: Optional[str], sub_assignments: UserAssignmentDict) -> AllocatedResource: - if assignment == 'NC': # not connected, create ideal port - return AllocatedResource(port_type(), port_name, 'NC', None) - - if assignment is not None: # filter the available resources to the assigned ones - allowed_resources = resources_by_name.get(assignment, []) - resource_pool = [resource for resource in resource_pool if resource in allowed_resources] - if not resource_pool: # specifically is a bad assignment - raise BadUserAssignError(f"no available allocation for {port_name}: {assignment}") - - for resource in resource_pool: # given the available resources, assign the first one possible - allocated = try_allocate_resource(port_type, port_name, resource, sub_assignments) - mark_resource_used(resource) - return allocated - - raise AutomaticAllocationError(f"no available allocation for {port_name}: {assignment}") - - # process the ports with user-specified assignments first, to avoid potentially conflicting assigns - unallocated_port_types_names: List[Tuple[Type[Port], str]] = [] # unpacked version of port_type_names - for (port_type, port_names) in port_types_names: - for port_name in port_names: - if assignments.contains_elt(port_name): - assignment, sub_assignments = assignments.get_elt(port_name) - resource_pool = free_resources_by_type[port_type] - allocated_resources.append( - allocate_port_type(resource_pool, port_type, port_name, assignment, sub_assignments)) + root_assign = None + return (root_assign, UserAssignmentDict(dict_out, name)) + + def __init__(self, elts: Dict[str, List[Tuple[List[str], str]]], name: List[str]): + self.elts = elts + self.name = name + self.marked: Set[str] = set() + + def contains_elt(self, elt: str) -> bool: + """Returns whether an element is still in the dict, without marking the element as read.""" + return elt in self.elts + + def get_elt(self, elt: str) -> Tuple[Optional[str], "UserAssignmentDict"]: + """Returns the top-level assignment for an element (or None, if no entry), plus the recursive UserAssignDict. + Marks the element as read.""" + if elt in self.elts: + self.marked.add(elt) + return self._from_list(self.elts[elt], self.name + [elt]) else: - unallocated_port_types_names.append((port_type, port_name)) + return (None, UserAssignmentDict({}, self.name + [elt])) - assignments.check_empty() + def check_empty(self) -> None: + """Checks that all assignments have been processed, otherwise raises a BadUserAssignError.""" + unprocessed = set(self.elts.keys()).difference(self.marked) + if unprocessed: + raise BadUserAssignError(f"unprocessed assignments in {'.'.join(self.name)}: {unprocessed}") - # then automatically assign anything that wasn't user-specified - for (port_type, port_name) in unallocated_port_types_names: - resource_pool = free_resources_by_type[port_type] - allocated_resources.append( - allocate_port_type(resource_pool, port_type, port_name, None, assignments.get_elt(port_name)[1])) - return allocated_resources +class PinMapUtil: + """Pin mapping utility, that takes in a definition of resources (pins and peripherals on a chip) and assigns them + automatically, optionally with user-defined explicit assignments.""" + + def __init__(self, resources: List[BasePinMapResource], transforms: Optional[PortTransformsType] = None): + self.resources = resources + self.transforms = DefaultPortTransforms if transforms is None else transforms + + def add(self, additional_resources: List[BasePinMapResource]) -> "PinMapUtil": + """Returns a new PinMapUtil with additional resources.""" + return PinMapUtil(self.resources + additional_resources, self.transforms) + + def remap_pins(self, pinmap: Dict[str, str]) -> "PinMapUtil": + """Returns a new PinMapUtil with pin names remapped according to the argument dict. Useful for a chip series + to define a generic pin mapping using pin names, and then remap those to pin numbers for specific packages. + Pins that are not in the pinmap are discarded.""" + + def remap_resource(resource: BasePinMapResource) -> Optional[BasePinMapResource]: + if isinstance(resource, PinResource): + if resource.pin in pinmap: + return PinResource(pinmap[resource.pin], resource.name_models) + else: + return None + elif isinstance(resource, PeripheralFixedPin): + remapped_pins = { + elt_name: pinmap[elt_pin] + for elt_name, elt_pin in resource.inner_allowed_pins.items() + if elt_pin in pinmap + } + return PeripheralFixedPin(resource.name, resource.port_model, remapped_pins) + elif isinstance(resource, BaseDelegatingPinMapResource): + return resource + else: + raise NotImplementedError(f"unknown resource {resource}") + + remapped_resources_raw = [remap_resource(resource) for resource in self.resources] + remapped_resources = [resource for resource in remapped_resources_raw if resource is not None] + + def resource_pin(resource: BasePinMapResource) -> List[str]: + if isinstance(resource, PinResource): + return [resource.pin] + elif isinstance(resource, PeripheralFixedPin): + return list(resource.inner_allowed_pins.values()) + elif isinstance(resource, BaseDelegatingPinMapResource): + return [] + else: + raise NotImplementedError(f"unknown resource {resource}") + + all_pins = itertools.chain(*[resource_pin(resource) for resource in self.resources]) + missed_names = set(pinmap.keys()).difference(all_pins) + assert not missed_names, f"invalid pins in remap: {missed_names}" + + return PinMapUtil(remapped_resources, self.transforms) + + @staticmethod + def _resource_port_types(resource: BasePinMapResource) -> List[Type[Port]]: + if isinstance(resource, PinResource): + return [type(model) for resource_name, model in resource.name_models.items()] + elif isinstance(resource, (PeripheralFixedPin, PeripheralAnyResource, PeripheralFixedResource)): + return [type(resource.port_model)] + else: + raise NotImplementedError(f"unsupported resource type {resource}") + + @staticmethod + def _resource_names(resource: BasePinMapResource) -> List[str]: + if isinstance(resource, PinResource): + return [resource.pin] + [resource_name for resource_name, model in resource.name_models.items()] + elif isinstance(resource, (PeripheralFixedPin, PeripheralAnyResource, PeripheralFixedResource)): + return [resource.name] + else: + raise NotImplementedError(f"unsupported resource type {resource}") + + def allocate( + self, port_types_names: List[Tuple[Type[Port], List[str]]], assignments_spec: List[str] = [] + ) -> List[AllocatedResource]: + """Performs port assignment given a list of port types and their names, and optional user-defined pin assignments + (which may be empty). Names may be duplicated (either within a port type, or across port types), and multiple + records will show up accordingly in the output data structure. + Returns a list of assigned ports, structured as a port model (set recursively for bundle ports), the input name, + and pin assignment as a pin string for leaf ports, or a dict (possibly recursive) for bundles.""" + assignments = UserAssignmentDict.from_spec(assignments_spec) + + # mutable data structure, resources will be removed as they are assigned + free_resources_by_type: Dict[Type[Port], List[BasePinMapResource]] = {} + for resource in self.resources: + for supported_type in self._resource_port_types(resource): + free_resources_by_type.setdefault(supported_type, []).append(resource) + + def mark_resource_used(resource: BasePinMapResource) -> None: + for supported_type in self._resource_port_types(resource): + free_resources_by_type[supported_type].remove(resource) + + # unlike the above, resources are not deleted from this + resources_by_name: Dict[str, List[BasePinMapResource]] = {} + for resource in self.resources: + for resource_name in self._resource_names(resource): + resources_by_name.setdefault(resource_name, []).append(resource) + + allocated_resources: List[AllocatedResource] = [] + + def try_allocate_resource( + port_type: Type[Port], port_name: str, resource: BasePinMapResource, sub_assignments: UserAssignmentDict + ) -> AllocatedResource: + """Try to assign a port to the specified resource, returning any resources used by this mapping and + the pin mapping. + Backtracking search is not implemented, this cannot fail.""" + if isinstance(resource, PinResource): # single pin: just assign it + sub_assignments.check_empty() + resource_name, resource_model = resource.get_name_model_for_type(port_type) + allocated_resource = AllocatedResource(resource_model, port_name, resource_name, resource.pin) + return allocated_resource + elif isinstance(resource, PeripheralFixedPin): # fixed pin: check user-assignment, or assign first + inner_pin_map: Dict[str, Tuple[str, Optional[str]]] = {} + for inner_name, inner_pin in resource.inner_allowed_pins.items(): # TODO should this be recursive? + inner_assignment, inner_sub_assignments = sub_assignments.get_elt(inner_name) + if inner_assignment is not None and inner_assignment != inner_pin: + raise BadUserAssignError(f"invalid assignment to {port_name}.{inner_name}: {inner_assignment}") + + inner_pin_map[inner_name] = (inner_pin, None) + inner_sub_assignments.check_empty() + + sub_assignments.check_empty() + allocated_resource = AllocatedResource(resource.port_model, port_name, resource.name, inner_pin_map) + return allocated_resource + elif isinstance(resource, (PeripheralAnyResource, PeripheralFixedResource)): # any-pin: delegate allocation + inner_pin_map = {} + resource_model = resource.port_model # typer gets confused if this is put directly where it is used + inner_models = {} + resource_name = resource.name # typer gets confused if this is put directly where it is used + for inner_name, inner_model in resource.port_model._ports.items(): + inner_type = type(inner_model) + if inner_type in self.transforms: # apply transform to search for the resource type, if needed + inner_type = self.transforms[inner_type][0] + + inner_assignment, inner_sub_assignments = sub_assignments.get_elt(inner_name) + + resource_pool = free_resources_by_type[inner_type] + if isinstance( + resource, PeripheralFixedResource + ): # filter further by allowed pins for this peripheral + allowed_names = resource.inner_allowed_names.get(inner_name, []) + allowed_resources = list( + itertools.chain(*[resources_by_name.get(name, []) for name in allowed_names]) + ) + resource_pool = [resource for resource in resource_pool if resource in allowed_resources] + + inner_allocation = allocate_port_type( + resource_pool, inner_type, f"{port_name}.{inner_name}", inner_assignment, inner_sub_assignments + ) + if inner_allocation.pin is not None: + assert isinstance(inner_allocation.pin, str) + inner_pin_map[inner_name] = (inner_allocation.pin, inner_allocation.resource_name) + + inner_models[inner_name] = inner_allocation.port_model + if ( + type(inner_model) in self.transforms + ): # apply transform to search for the resource type, if needed + inner_models[inner_name] = self.transforms[type(inner_model)][1](inner_models[inner_name]) + sub_assignments.check_empty() + resource_model = resource_model.with_elt_initializers(inner_models) + allocated_resource = AllocatedResource(resource_model, port_name, resource_name, inner_pin_map) + return allocated_resource + else: + raise NotImplementedError(f"unsupported resource type {resource}") + + # allocates a resource greedily (or errors out), and consumes the relevant resource + def allocate_port_type( + resource_pool: List[BasePinMapResource], + port_type: Type[Port], + port_name: str, + assignment: Optional[str], + sub_assignments: UserAssignmentDict, + ) -> AllocatedResource: + if assignment == "NC": # not connected, create ideal port + return AllocatedResource(port_type(), port_name, "NC", None) + + if assignment is not None: # filter the available resources to the assigned ones + allowed_resources = resources_by_name.get(assignment, []) + resource_pool = [resource for resource in resource_pool if resource in allowed_resources] + if not resource_pool: # specifically is a bad assignment + raise BadUserAssignError(f"no available allocation for {port_name}: {assignment}") + + for resource in resource_pool: # given the available resources, assign the first one possible + allocated = try_allocate_resource(port_type, port_name, resource, sub_assignments) + mark_resource_used(resource) + return allocated + + raise AutomaticAllocationError(f"no available allocation for {port_name}: {assignment}") + + # process the ports with user-specified assignments first, to avoid potentially conflicting assigns + unallocated_port_types_names: List[Tuple[Type[Port], str]] = [] # unpacked version of port_type_names + for port_type, port_names in port_types_names: + for port_name in port_names: + if assignments.contains_elt(port_name): + assignment, sub_assignments = assignments.get_elt(port_name) + resource_pool = free_resources_by_type[port_type] + allocated_resources.append( + allocate_port_type(resource_pool, port_type, port_name, assignment, sub_assignments) + ) + else: + unallocated_port_types_names.append((port_type, port_name)) + + assignments.check_empty() + + # then automatically assign anything that wasn't user-specified + for port_type, port_name in unallocated_port_types_names: + resource_pool = free_resources_by_type[port_type] + allocated_resources.append( + allocate_port_type(resource_pool, port_type, port_name, None, assignments.get_elt(port_name)[1]) + ) + + return allocated_resources diff --git a/edg/abstract_parts/PowerCircuits.py b/edg/abstract_parts/PowerCircuits.py index 9a03324be..b77ce7c8a 100644 --- a/edg/abstract_parts/PowerCircuits.py +++ b/edg/abstract_parts/PowerCircuits.py @@ -19,6 +19,7 @@ class HalfBridge(PowerConditioner, Block): """Half bridge circuit with logic-level inputs and current draw calculated from the output node. Two power rails: logic power (which can be used to power gate drivers), and the power rail.""" + def __init__(self) -> None: super().__init__() @@ -47,8 +48,10 @@ def __init__(self) -> None: @abstract_block class FetHalfBridge(HalfBridge): """Implementation of a half-bridge with two NFETs and a gate driver.""" - def __init__(self, frequency: RangeLike, fet_rds: RangeLike = (0, 1)*Ohm, - gate_res: RangeLike = 22*Ohm(tol=0.05)): + + def __init__( + self, frequency: RangeLike, fet_rds: RangeLike = (0, 1) * Ohm, gate_res: RangeLike = 22 * Ohm(tol=0.05) + ): super().__init__() self.frequency = self.ArgParameter(frequency) self.fet_rds = self.ArgParameter(fet_rds) @@ -65,48 +68,62 @@ def contents(self) -> None: gate_res_model = Resistor(self.gate_res) - self.low_fet = self.Block(SwitchFet.NFet( - drain_voltage=self.pwr.link().voltage, - drain_current=(0, self.out.link().current_drawn.upper()), - gate_voltage=self.driver.low_out.link().voltage, - rds_on=self.fet_rds, - frequency=self.frequency, - drive_current=self.driver.low_out.link().current_limits - )) + self.low_fet = self.Block( + SwitchFet.NFet( + drain_voltage=self.pwr.link().voltage, + drain_current=(0, self.out.link().current_drawn.upper()), + gate_voltage=self.driver.low_out.link().voltage, + rds_on=self.fet_rds, + frequency=self.frequency, + drive_current=self.driver.low_out.link().current_limits, + ) + ) self.connect(self.low_fet.source.adapt_to(Ground()), self.gnd) self.low_gate_res = self.Block(gate_res_model) - self.connect(self.low_gate_res.a.adapt_to(DigitalSink( - current_draw=self.low_fet.actual_gate_charge * self.frequency.hull((0, 0)) - )), self.driver.low_out) + self.connect( + self.low_gate_res.a.adapt_to( + DigitalSink(current_draw=self.low_fet.actual_gate_charge * self.frequency.hull((0, 0))) + ), + self.driver.low_out, + ) self.connect(self.low_gate_res.b, self.low_fet.gate) - self.high_fet = self.Block(SwitchFet.NFet( - drain_voltage=self.pwr.link().voltage, - drain_current=(0, self.out.link().current_drawn.upper()), - gate_voltage=self.driver.high_out.link().voltage - self.driver.high_gnd.link().voltage, - rds_on=self.fet_rds, - frequency=self.frequency, - drive_current=self.driver.high_out.link().current_limits - )) - self.connect(self.high_fet.drain.adapt_to(VoltageSink( - voltage_limits=self.high_fet.actual_drain_voltage_rating, - current_draw=self.out.link().current_drawn - )), self.pwr) + self.high_fet = self.Block( + SwitchFet.NFet( + drain_voltage=self.pwr.link().voltage, + drain_current=(0, self.out.link().current_drawn.upper()), + gate_voltage=self.driver.high_out.link().voltage - self.driver.high_gnd.link().voltage, + rds_on=self.fet_rds, + frequency=self.frequency, + drive_current=self.driver.high_out.link().current_limits, + ) + ) + self.connect( + self.high_fet.drain.adapt_to( + VoltageSink( + voltage_limits=self.high_fet.actual_drain_voltage_rating, current_draw=self.out.link().current_drawn + ) + ), + self.pwr, + ) self.high_gate_res = self.Block(gate_res_model) - self.connect(self.high_gate_res.a.adapt_to(DigitalSink( - current_draw=self.high_fet.actual_gate_charge * self.frequency.hull((0, 0)) - )), self.driver.high_out) + self.connect( + self.high_gate_res.a.adapt_to( + DigitalSink(current_draw=self.high_fet.actual_gate_charge * self.frequency.hull((0, 0))) + ), + self.driver.high_out, + ) self.connect(self.high_gate_res.b, self.high_fet.gate) # to avoid tolerance stackup, model the switch node as a static voltage self.connect(self.low_fet.drain, self.high_fet.source) - self.connect(self.low_fet.drain.adapt_to(VoltageSource( - voltage_out=self.pwr.link().voltage)), - self.out) - self.connect(self.out.as_ground((0, 0)*Amp), self.driver.high_gnd) # TODO model driver current + self.connect(self.low_fet.drain.adapt_to(VoltageSource(voltage_out=self.pwr.link().voltage)), self.out) + self.connect(self.out.as_ground((0, 0) * Amp), self.driver.high_gnd) # TODO model driver current - self.assign(self.actual_current_limits, self.low_fet.actual_drain_current_rating.intersect( - self.high_fet.actual_drain_current_rating)) + self.assign( + self.actual_current_limits, + self.low_fet.actual_drain_current_rating.intersect(self.high_fet.actual_drain_current_rating), + ) class FetHalfBridgeIndependent(FetHalfBridge, HalfBridgeIndependent): @@ -167,9 +184,16 @@ class RampLimiter(KiCadSchematicBlock): Additional more complex circuits https://electronics.stackexchange.com/questions/294061/p-channel-mosfet-inrush-current-limiting """ - def __init__(self, *, cgd: RangeLike = 10*nFarad(tol=0.5), target_ramp: RangeLike = 1000*Volt(tol=0.25), - target_vgs: RangeLike = (4, 10)*Volt, max_rds: FloatLike = 1*Ohm, - _cdiv_vgs_factor: RangeLike = (0.05, 0.75)): + + def __init__( + self, + *, + cgd: RangeLike = 10 * nFarad(tol=0.5), + target_ramp: RangeLike = 1000 * Volt(tol=0.25), + target_vgs: RangeLike = (4, 10) * Volt, + max_rds: FloatLike = 1 * Ohm, + _cdiv_vgs_factor: RangeLike = (0.05, 0.75), + ): super().__init__() self.gnd = self.Port(Ground.empty(), [Common]) @@ -188,49 +212,57 @@ def contents(self) -> None: super().contents() pwr_voltage = self.pwr_in.link().voltage - self.drv = self.Block(SwitchFet.PFet( - drain_voltage=pwr_voltage, - drain_current=self.pwr_out.link().current_drawn, - gate_voltage=(0 * Volt(tol=0)).hull(self.target_vgs.upper()), - gate_threshold_voltage=(0 * Volt(tol=0)).hull(self.target_vgs.lower()), - rds_on=(0, self.max_rds) - )) - - self.cap_gd = self.Block(Capacitor( - capacitance=self.cgd, - voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage) - )) + self.drv = self.Block( + SwitchFet.PFet( + drain_voltage=pwr_voltage, + drain_current=self.pwr_out.link().current_drawn, + gate_voltage=(0 * Volt(tol=0)).hull(self.target_vgs.upper()), + gate_threshold_voltage=(0 * Volt(tol=0)).hull(self.target_vgs.lower()), + rds_on=(0, self.max_rds), + ) + ) + + self.cap_gd = self.Block( + Capacitor(capacitance=self.cgd, voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage)) + ) # treat Cgs and Cgd as a capacitive divider with Cgs on the bottom - self.cap_gs = self.Block(Capacitor( - capacitance=( - (1/(self.drv.actual_gate_drive.lower()*self._cdiv_vgs_factor)).shrink_multiply(self.pwr_in.link().voltage) - 1 - ).shrink_multiply( - self.cap_gd.actual_capacitance - ), - voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage) - )) + self.cap_gs = self.Block( + Capacitor( + capacitance=( + (1 / (self.drv.actual_gate_drive.lower() * self._cdiv_vgs_factor)).shrink_multiply( + self.pwr_in.link().voltage + ) + - 1 + ).shrink_multiply(self.cap_gd.actual_capacitance), + voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage), + ) + ) # dV/dt over a capacitor is I / C => I = Cgd * dV/dt # then calculate to get the target I: Vgs,th = I * Reff => Reff = Vgs,th / I = Vgs,th / (Cgd * dV/dt) # we assume Vgs,th is exact, and only contributing sources come from elsewhere - self.div = self.Block(ResistiveDivider(ratio=self.target_vgs.shrink_multiply(1/self.pwr_in.link().voltage), - impedance=(1 / self.target_ramp).shrink_multiply(self.drv.actual_gate_drive.lower() / (self.cap_gd.actual_capacitance)) - )) - div_current_draw = (self.pwr_in.link().voltage/self.div.actual_impedance).hull(0) - self.ctl_fet = self.Block(SwitchFet.NFet( - drain_voltage=pwr_voltage, - drain_current=div_current_draw, - gate_voltage=(self.control.link().output_thresholds.upper(), self.control.link().voltage.upper()) - )) + self.div = self.Block( + ResistiveDivider( + ratio=self.target_vgs.shrink_multiply(1 / self.pwr_in.link().voltage), + impedance=(1 / self.target_ramp).shrink_multiply( + self.drv.actual_gate_drive.lower() / (self.cap_gd.actual_capacitance) + ), + ) + ) + div_current_draw = (self.pwr_in.link().voltage / self.div.actual_impedance).hull(0) + self.ctl_fet = self.Block( + SwitchFet.NFet( + drain_voltage=pwr_voltage, + drain_current=div_current_draw, + gate_voltage=(self.control.link().output_thresholds.upper(), self.control.link().voltage.upper()), + ) + ) self.import_kicad( self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), conversions={ - 'pwr_in': VoltageSink( - current_draw=self.pwr_out.link().current_drawn + div_current_draw - ), - 'pwr_out': VoltageSource( - voltage_out=self.pwr_in.link().voltage - ), - 'control': DigitalSink(), - 'gnd': Ground(), - }) + "pwr_in": VoltageSink(current_draw=self.pwr_out.link().current_drawn + div_current_draw), + "pwr_out": VoltageSource(voltage_out=self.pwr_in.link().voltage), + "control": DigitalSink(), + "gnd": Ground(), + }, + ) diff --git a/edg/abstract_parts/Resettable.py b/edg/abstract_parts/Resettable.py index 97e4a3794..2a0a256d2 100644 --- a/edg/abstract_parts/Resettable.py +++ b/edg/abstract_parts/Resettable.py @@ -19,6 +19,7 @@ class Resettable(BlockInterfaceMixin[Block]): Microcontrollers may generate internal programming connectors that drive this signal, and system designers must connect microcontroller resets with this in mind - for example, only driving them in open-drain mode. """ + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.reset = self.Port(DigitalSink.empty(), optional=True) diff --git a/edg/abstract_parts/ResistiveDivider.py b/edg/abstract_parts/ResistiveDivider.py index d38b0388d..17ab56988 100644 --- a/edg/abstract_parts/ResistiveDivider.py +++ b/edg/abstract_parts/ResistiveDivider.py @@ -11,245 +11,280 @@ from .ESeriesUtil import ESeriesUtil, ESeriesRatioUtil, ESeriesRatioValue -class DividerValues(ESeriesRatioValue['DividerValues']): - """Resistive divider calculator using the ESeriesRatioUtil infrastructure. - - R1 is the high-side resistor, and R2 is the low-side resistor, such that - Vout = Vin * R2 / (R1 + R2) - - Example of decade adjustment: - R1 : R2 maxR2/minR1 minR2/maxR1 (theoretical) - 1 : 10 100/101 10/20 /\ ratio towards 1 - 1 : 1 10/11 1/11 - 10 : 1 10/20 1/101 - 100: 1 10/110 1/1001 \/ ratio towards 0 - """ - def __init__(self, ratio: Range, parallel_impedance: Range): - self.ratio = ratio # amplification factor from in to out - self.parallel_impedance = parallel_impedance # parallel impedance into the opamp negative pin - - @staticmethod - @override - def from_resistors(r1_range: Range, r2_range: Range) -> 'DividerValues': - """This uses a slight rewriting of terms to avoid duplication of terms and not double-count tolerances: - ratio = R2 / (R1 + R2) => divide through by R2 / R2 - ratio = 1 / (R1 / R2 + 1) +class DividerValues(ESeriesRatioValue["DividerValues"]): + """Resistive divider calculator using the ESeriesRatioUtil infrastructure. + + R1 is the high-side resistor, and R2 is the low-side resistor, such that + Vout = Vin * R2 / (R1 + R2) + + Example of decade adjustment: + R1 : R2 maxR2/minR1 minR2/maxR1 (theoretical) + 1 : 10 100/101 10/20 /\ ratio towards 1 + 1 : 1 10/11 1/11 + 10 : 1 10/20 1/101 + 100: 1 10/110 1/1001 \/ ratio towards 0 """ - return DividerValues( - 1 / (r1_range / r2_range + 1), - 1 / (1 / r1_range + 1 / r2_range) - ) - - @override - def initial_test_decades(self) -> Tuple[int, int]: - decade = ceil(log10(self.parallel_impedance.center())) - return decade, decade - - @override - def distance_to(self, spec: 'DividerValues') -> List[float]: - if self.ratio in spec.ratio and self.parallel_impedance in spec.parallel_impedance: - return [] - else: - return [ - abs(self.ratio.center() - spec.ratio.center()), - abs(self.parallel_impedance.center() - spec.parallel_impedance.center()) - ] - - @override - def intersects(self, spec: 'DividerValues') -> bool: - return self.ratio.intersects(spec.ratio) and \ - self.parallel_impedance.intersects(spec.parallel_impedance) + + def __init__(self, ratio: Range, parallel_impedance: Range): + self.ratio = ratio # amplification factor from in to out + self.parallel_impedance = parallel_impedance # parallel impedance into the opamp negative pin + + @staticmethod + @override + def from_resistors(r1_range: Range, r2_range: Range) -> "DividerValues": + """This uses a slight rewriting of terms to avoid duplication of terms and not double-count tolerances: + ratio = R2 / (R1 + R2) => divide through by R2 / R2 + ratio = 1 / (R1 / R2 + 1) + """ + return DividerValues(1 / (r1_range / r2_range + 1), 1 / (1 / r1_range + 1 / r2_range)) + + @override + def initial_test_decades(self) -> Tuple[int, int]: + decade = ceil(log10(self.parallel_impedance.center())) + return decade, decade + + @override + def distance_to(self, spec: "DividerValues") -> List[float]: + if self.ratio in spec.ratio and self.parallel_impedance in spec.parallel_impedance: + return [] + else: + return [ + abs(self.ratio.center() - spec.ratio.center()), + abs(self.parallel_impedance.center() - spec.parallel_impedance.center()), + ] + + @override + def intersects(self, spec: "DividerValues") -> bool: + return self.ratio.intersects(spec.ratio) and self.parallel_impedance.intersects(spec.parallel_impedance) class ResistiveDivider(InternalSubcircuit, KiCadImportableBlock, GeneratorBlock): - """Abstract, untyped (Passive) resistive divider, that takes in a ratio and parallel impedance spec.""" - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name == 'Device:VoltageDivider' - return {'1': self.top, '2': self.center, '3': self.bottom} - - @classmethod - def divider_ratio(cls, rtop: RangeExpr, rbot: RangeExpr) -> RangeExpr: - """Calculates the output voltage of a resistive divider given the input voltages and resistances.""" - return 1 / (rtop / rbot + 1) - - @classmethod - def divider_output(cls, vtop: RangeExpr, vbot: RangeExpr, ratio: RangeExpr) -> RangeExpr: - """Calculates the output voltage of a resistive divider given the input voltages and resistances.""" - return RangeExpr._to_expr_type(((vtop.lower() - vbot.lower()) * ratio.lower() + vbot.lower(), - (vtop.upper() - vbot.upper()) * ratio.upper() + vbot.upper())) - - def __init__(self, ratio: RangeLike, impedance: RangeLike, *, - series: IntLike = 24, tolerance: FloatLike = 0.01) -> None: - super().__init__() - - self.ratio = self.ArgParameter(ratio) - self.impedance = self.ArgParameter(impedance) - self.series = self.ArgParameter(series) - self.tolerance = self.ArgParameter(tolerance) - self.generator_param(self.ratio, self.impedance, self.series, self.tolerance) - - self.actual_rtop = self.Parameter(RangeExpr()) - self.actual_rbot = self.Parameter(RangeExpr()) - self.actual_ratio = self.Parameter(RangeExpr()) - self.actual_impedance = self.Parameter(RangeExpr()) - self.actual_series_impedance = self.Parameter(RangeExpr()) - - self.top = self.Port(Passive.empty()) - self.center = self.Port(Passive.empty()) - self.bottom = self.Port(Passive.empty()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "ratio: ", DescriptionString.FormatUnits(self.actual_ratio, ""), - " of spec ", DescriptionString.FormatUnits(self.ratio, ""), - "\nimpedance: ", DescriptionString.FormatUnits(self.actual_impedance, "Ω"), - " of spec: ", DescriptionString.FormatUnits(self.impedance, "Ω")) - - - @override - def generate(self) -> None: - """Generates a resistive divider meeting the required specifications, with the lowest E-series resistors possible. - """ - super().generate() - - # TODO: not fully optimal in that the ratio doesn't need to be recalculated if we're shifting both decades - # (to achieve some impedance spec), but it uses shared infrastructure that doesn't assume this ratio optimization - calculator = ESeriesRatioUtil(ESeriesUtil.SERIES[self.get(self.series)], self.get(self.tolerance), DividerValues) - top_resistance, bottom_resistance = calculator.find(DividerValues(self.get(self.ratio), self.get(self.impedance))) - - self.top_res = self.Block(Resistor( - resistance=Range.from_tolerance(top_resistance, self.get(self.tolerance)) - )) - self.bottom_res = self.Block(Resistor( - resistance=Range.from_tolerance(bottom_resistance, self.get(self.tolerance)) - )) - - self.connect(self.top_res.a, self.top) - self.connect(self.top_res.b, self.center, self.bottom_res.a) - self.connect(self.bottom_res.b, self.bottom) - - self.assign(self.actual_rtop, self.top_res.actual_resistance) - self.assign(self.actual_rbot, self.bottom_res.actual_resistance) - self.assign(self.actual_impedance, - 1 / (1 / self.top_res.actual_resistance + 1 / self.bottom_res.actual_resistance)) - self.assign(self.actual_series_impedance, - self.top_res.actual_resistance + self.bottom_res.actual_resistance) - self.assign(self.actual_ratio, - self.divider_ratio(self.top_res.actual_resistance, self.bottom_res.actual_resistance)) + """Abstract, untyped (Passive) resistive divider, that takes in a ratio and parallel impedance spec.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name == "Device:VoltageDivider" + return {"1": self.top, "2": self.center, "3": self.bottom} + + @classmethod + def divider_ratio(cls, rtop: RangeExpr, rbot: RangeExpr) -> RangeExpr: + """Calculates the output voltage of a resistive divider given the input voltages and resistances.""" + return 1 / (rtop / rbot + 1) + + @classmethod + def divider_output(cls, vtop: RangeExpr, vbot: RangeExpr, ratio: RangeExpr) -> RangeExpr: + """Calculates the output voltage of a resistive divider given the input voltages and resistances.""" + return RangeExpr._to_expr_type( + ( + (vtop.lower() - vbot.lower()) * ratio.lower() + vbot.lower(), + (vtop.upper() - vbot.upper()) * ratio.upper() + vbot.upper(), + ) + ) + + def __init__( + self, ratio: RangeLike, impedance: RangeLike, *, series: IntLike = 24, tolerance: FloatLike = 0.01 + ) -> None: + super().__init__() + + self.ratio = self.ArgParameter(ratio) + self.impedance = self.ArgParameter(impedance) + self.series = self.ArgParameter(series) + self.tolerance = self.ArgParameter(tolerance) + self.generator_param(self.ratio, self.impedance, self.series, self.tolerance) + + self.actual_rtop = self.Parameter(RangeExpr()) + self.actual_rbot = self.Parameter(RangeExpr()) + self.actual_ratio = self.Parameter(RangeExpr()) + self.actual_impedance = self.Parameter(RangeExpr()) + self.actual_series_impedance = self.Parameter(RangeExpr()) + + self.top = self.Port(Passive.empty()) + self.center = self.Port(Passive.empty()) + self.bottom = self.Port(Passive.empty()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "ratio: ", + DescriptionString.FormatUnits(self.actual_ratio, ""), + " of spec ", + DescriptionString.FormatUnits(self.ratio, ""), + "\nimpedance: ", + DescriptionString.FormatUnits(self.actual_impedance, "Ω"), + " of spec: ", + DescriptionString.FormatUnits(self.impedance, "Ω"), + ) + + @override + def generate(self) -> None: + """Generates a resistive divider meeting the required specifications, with the lowest E-series resistors possible.""" + super().generate() + + # TODO: not fully optimal in that the ratio doesn't need to be recalculated if we're shifting both decades + # (to achieve some impedance spec), but it uses shared infrastructure that doesn't assume this ratio optimization + calculator = ESeriesRatioUtil( + ESeriesUtil.SERIES[self.get(self.series)], self.get(self.tolerance), DividerValues + ) + top_resistance, bottom_resistance = calculator.find( + DividerValues(self.get(self.ratio), self.get(self.impedance)) + ) + + self.top_res = self.Block(Resistor(resistance=Range.from_tolerance(top_resistance, self.get(self.tolerance)))) + self.bottom_res = self.Block( + Resistor(resistance=Range.from_tolerance(bottom_resistance, self.get(self.tolerance))) + ) + + self.connect(self.top_res.a, self.top) + self.connect(self.top_res.b, self.center, self.bottom_res.a) + self.connect(self.bottom_res.b, self.bottom) + + self.assign(self.actual_rtop, self.top_res.actual_resistance) + self.assign(self.actual_rbot, self.bottom_res.actual_resistance) + self.assign( + self.actual_impedance, 1 / (1 / self.top_res.actual_resistance + 1 / self.bottom_res.actual_resistance) + ) + self.assign(self.actual_series_impedance, self.top_res.actual_resistance + self.bottom_res.actual_resistance) + self.assign( + self.actual_ratio, self.divider_ratio(self.top_res.actual_resistance, self.bottom_res.actual_resistance) + ) @non_library class BaseVoltageDivider(KiCadImportableBlock): - """Base class that defines a resistive divider that takes in a voltage source and ground, and outputs - an analog constant-voltage signal. - The actual output voltage is defined as a ratio of the input voltage, and the divider is specified by - ratio and impedance. - Subclasses should define the ratio and impedance spec.""" - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name == 'Device:VoltageDivider' - return {'1': self.input, '2': self.output, '3': self.gnd} - - def __init__(self, impedance: RangeLike) -> None: - super().__init__() - - self.impedance = self.ArgParameter(impedance) - self.ratio = self.Parameter(RangeExpr()) # "internal" forward-declared parameter - self.div = self.Block(ResistiveDivider(ratio=self.ratio, impedance=impedance)) - - self.gnd = self.Export(self.div.bottom.adapt_to(Ground()), [Common]) - self.input = self.Port(VoltageSink.empty(), [Input]) # forward declaration only - output_voltage = ResistiveDivider.divider_output(self.input.link().voltage, self.gnd.link().voltage, self.div.actual_ratio) - self.output = self.Export(self.div.center.adapt_to(AnalogSource( - voltage_out=output_voltage, - signal_out=output_voltage, - current_limits=RangeExpr.ALL, - impedance=self.div.actual_impedance - )), [Output]) - self.connect(self.input, self.div.top.adapt_to(VoltageSink( - current_draw=self.output.link().current_drawn, - voltage_limits=RangeExpr.ALL - ))) - - self.actual_rtop = self.Parameter(RangeExpr(self.div.actual_rtop)) - self.actual_rbot = self.Parameter(RangeExpr(self.div.actual_rbot)) - self.actual_ratio = self.Parameter(RangeExpr(self.div.actual_ratio)) - self.actual_impedance = self.Parameter(RangeExpr(self.div.actual_impedance)) - self.actual_series_impedance = self.Parameter(RangeExpr(self.div.actual_series_impedance)) + """Base class that defines a resistive divider that takes in a voltage source and ground, and outputs + an analog constant-voltage signal. + The actual output voltage is defined as a ratio of the input voltage, and the divider is specified by + ratio and impedance. + Subclasses should define the ratio and impedance spec.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name == "Device:VoltageDivider" + return {"1": self.input, "2": self.output, "3": self.gnd} + + def __init__(self, impedance: RangeLike) -> None: + super().__init__() + + self.impedance = self.ArgParameter(impedance) + self.ratio = self.Parameter(RangeExpr()) # "internal" forward-declared parameter + self.div = self.Block(ResistiveDivider(ratio=self.ratio, impedance=impedance)) + + self.gnd = self.Export(self.div.bottom.adapt_to(Ground()), [Common]) + self.input = self.Port(VoltageSink.empty(), [Input]) # forward declaration only + output_voltage = ResistiveDivider.divider_output( + self.input.link().voltage, self.gnd.link().voltage, self.div.actual_ratio + ) + self.output = self.Export( + self.div.center.adapt_to( + AnalogSource( + voltage_out=output_voltage, + signal_out=output_voltage, + current_limits=RangeExpr.ALL, + impedance=self.div.actual_impedance, + ) + ), + [Output], + ) + self.connect( + self.input, + self.div.top.adapt_to( + VoltageSink(current_draw=self.output.link().current_drawn, voltage_limits=RangeExpr.ALL) + ), + ) + + self.actual_rtop = self.Parameter(RangeExpr(self.div.actual_rtop)) + self.actual_rbot = self.Parameter(RangeExpr(self.div.actual_rbot)) + self.actual_ratio = self.Parameter(RangeExpr(self.div.actual_ratio)) + self.actual_impedance = self.Parameter(RangeExpr(self.div.actual_impedance)) + self.actual_series_impedance = self.Parameter(RangeExpr(self.div.actual_series_impedance)) class VoltageDivider(Analog, BaseVoltageDivider): - """Voltage divider that takes in an output voltage and parallel impedance spec, and produces an output analog signal - of the appropriate magnitude (as a fraction of the input voltage)""" - def __init__(self, *, output_voltage: RangeLike, impedance: RangeLike) -> None: - super().__init__(impedance=impedance) - self.output_voltage = self.ArgParameter(output_voltage) - self.assign(self.ratio, self.output_voltage.shrink_multiply(1/self.input.link().voltage)) + """Voltage divider that takes in an output voltage and parallel impedance spec, and produces an output analog signal + of the appropriate magnitude (as a fraction of the input voltage)""" + + def __init__(self, *, output_voltage: RangeLike, impedance: RangeLike) -> None: + super().__init__(impedance=impedance) + self.output_voltage = self.ArgParameter(output_voltage) + self.assign(self.ratio, self.output_voltage.shrink_multiply(1 / self.input.link().voltage)) class VoltageSenseDivider(Analog, BaseVoltageDivider): - """Voltage divider that takes in an output voltage and parallel impedance spec, and produces an output analog signal - of the appropriate magnitude (as a fraction of the input voltage). - Unlike the normal VoltageDivider, the output is defined in terms of full scale voltage - that is, the voltage - output at the maximum input voltage, which makes the tolerance specification more useful for sensing applications - with variable input voltage. + """Voltage divider that takes in an output voltage and parallel impedance spec, and produces an output analog signal + of the appropriate magnitude (as a fraction of the input voltage). + Unlike the normal VoltageDivider, the output is defined in terms of full scale voltage - that is, the voltage + output at the maximum input voltage, which makes the tolerance specification more useful for sensing applications + with variable input voltage. - TODO: can this be unified with VoltageDivider?""" - def __init__(self, *, full_scale_voltage: RangeLike, impedance: RangeLike) -> None: - super().__init__(impedance=impedance) - self.full_scale_voltage = self.ArgParameter(full_scale_voltage) - self.assign(self.ratio, self.full_scale_voltage / self.input.link().voltage.upper()) + TODO: can this be unified with VoltageDivider?""" + + def __init__(self, *, full_scale_voltage: RangeLike, impedance: RangeLike) -> None: + super().__init__(impedance=impedance) + self.full_scale_voltage = self.ArgParameter(full_scale_voltage) + self.assign(self.ratio, self.full_scale_voltage / self.input.link().voltage.upper()) class FeedbackVoltageDivider(Analog, BaseVoltageDivider): - """Voltage divider that takes in a ratio and parallel impedance spec, and produces an output analog signal - of the appropriate magnitude (as a fraction of the input voltage)""" - def __init__(self, *, output_voltage: RangeLike, impedance: RangeLike, - assumed_input_voltage: RangeLike) -> None: - super().__init__(impedance=impedance) + """Voltage divider that takes in a ratio and parallel impedance spec, and produces an output analog signal + of the appropriate magnitude (as a fraction of the input voltage)""" + + def __init__(self, *, output_voltage: RangeLike, impedance: RangeLike, assumed_input_voltage: RangeLike) -> None: + super().__init__(impedance=impedance) - self.output_voltage = self.ArgParameter(output_voltage) - self.assumed_input_voltage = self.ArgParameter(assumed_input_voltage) - self.actual_input_voltage = self.Parameter(RangeExpr()) + self.output_voltage = self.ArgParameter(output_voltage) + self.assumed_input_voltage = self.ArgParameter(assumed_input_voltage) + self.actual_input_voltage = self.Parameter(RangeExpr()) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.description = DescriptionString( # TODO forward from internal? - "ratio: ", DescriptionString.FormatUnits(self.actual_ratio, ""), - " of spec ", DescriptionString.FormatUnits(self.ratio, ""), - "\nimpedance: ", DescriptionString.FormatUnits(self.actual_impedance, "Ω"), - " of spec: ", DescriptionString.FormatUnits(self.impedance, "Ω")) + self.description = DescriptionString( # TODO forward from internal? + "ratio: ", + DescriptionString.FormatUnits(self.actual_ratio, ""), + " of spec ", + DescriptionString.FormatUnits(self.ratio, ""), + "\nimpedance: ", + DescriptionString.FormatUnits(self.actual_impedance, "Ω"), + " of spec: ", + DescriptionString.FormatUnits(self.impedance, "Ω"), + ) - self.assign(self.ratio, (1/self.assumed_input_voltage).shrink_multiply(self.output_voltage)) - self.assign(self.actual_input_voltage, self.output_voltage / self.actual_ratio) + self.assign(self.ratio, (1 / self.assumed_input_voltage).shrink_multiply(self.output_voltage)) + self.assign(self.actual_input_voltage, self.output_voltage / self.actual_ratio) class SignalDivider(Analog, KiCadImportableBlock, Block): - """Specialization of ResistiveDivider for Analog signals""" - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name == 'Device:VoltageDivider' - return {'1': self.input, '2': self.output, '3': self.gnd} - - def __init__(self, ratio: RangeLike, impedance: RangeLike) -> None: - super().__init__() - - self.div = self.Block(ResistiveDivider(ratio=ratio, impedance=impedance)) - self.gnd = self.Export(self.div.bottom.adapt_to(Ground()), [Common]) - self.input = self.Port(AnalogSink.empty(), [Input]) # forward declaration - output_voltage = ResistiveDivider.divider_output(self.input.link().voltage, self.gnd.link().voltage, self.div.actual_ratio) - self.output = self.Export(self.div.center.adapt_to(AnalogSource( - voltage_out=output_voltage, - signal_out=output_voltage, - impedance=self.div.actual_impedance - )), [Output]) - self.connect(self.input, self.div.top.adapt_to(AnalogSink( - impedance=self.div.actual_series_impedance, - current_draw=self.output.link().current_drawn, - ))) + """Specialization of ResistiveDivider for Analog signals""" + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name == "Device:VoltageDivider" + return {"1": self.input, "2": self.output, "3": self.gnd} + + def __init__(self, ratio: RangeLike, impedance: RangeLike) -> None: + super().__init__() + + self.div = self.Block(ResistiveDivider(ratio=ratio, impedance=impedance)) + self.gnd = self.Export(self.div.bottom.adapt_to(Ground()), [Common]) + self.input = self.Port(AnalogSink.empty(), [Input]) # forward declaration + output_voltage = ResistiveDivider.divider_output( + self.input.link().voltage, self.gnd.link().voltage, self.div.actual_ratio + ) + self.output = self.Export( + self.div.center.adapt_to( + AnalogSource(voltage_out=output_voltage, signal_out=output_voltage, impedance=self.div.actual_impedance) + ), + [Output], + ) + self.connect( + self.input, + self.div.top.adapt_to( + AnalogSink( + impedance=self.div.actual_series_impedance, + current_draw=self.output.link().current_drawn, + ) + ), + ) diff --git a/edg/abstract_parts/RfNetworks.py b/edg/abstract_parts/RfNetworks.py index 7c7a38b12..98b252fef 100644 --- a/edg/abstract_parts/RfNetworks.py +++ b/edg/abstract_parts/RfNetworks.py @@ -15,6 +15,7 @@ class DiscreteRfWarning(BlockInterfaceMixin[Block]): parasitics of real devices. The discrete RF library components / generators are also experimental and subject to change. They also do not adhere to the tolerance conventions of non-RF parts.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.discrete_rf_warning = self.Parameter(BoolExpr(False)) @@ -41,21 +42,20 @@ def _calculate_impedance(cls, z1: complex, z2: complex) -> Tuple[float, float]: xp2 = rp2 / q2 # parallel capacitance aka cstray else: rp2 = z2.real - xp2 = float('inf') # for real impedance, no stray capacitance + xp2 = float("inf") # for real impedance, no stray capacitance q = sqrt(rp2 / rp1 - 1) net_xp = -rp2 / q # TODO: where is the negative sign coming from net_xs = q * rp1 - return net_xs - xp1, 1/(1/net_xp - 1/xp2) + return net_xs - xp1, 1 / (1 / net_xp - 1 / xp2) @classmethod def _calculate_values(cls, freq: float, z1: complex, z2: complex) -> Tuple[float, float]: """Calculate a L matching network for complex Z1 (series-inductor side) and Z2 (parallel-capacitor side) and returns L, C""" xs, xp = cls._calculate_impedance(z1, z2) - return PiLowPassFilter._reactance_to_inductance(freq, xs),\ - PiLowPassFilter._reactance_to_capacitance(freq, xp) + return PiLowPassFilter._reactance_to_inductance(freq, xs), PiLowPassFilter._reactance_to_capacitance(freq, xp) class LLowPassFilterWith2HNotch(GeneratorBlock, RfFilter): @@ -70,17 +70,25 @@ class LLowPassFilterWith2HNotch(GeneratorBlock, RfFilter): w_bp = 1/(sqrt(l*c)) solving both gives a new L of 3/4 the baseline L """ + @classmethod def _calculate_values(cls, freq: float, z1: complex, z2: complex) -> Tuple[float, float, float]: """Returns L, Cp, Clc""" l, c = LLowPassFilter._calculate_values(freq, z1, z2) lc_l = l * 3 / 4 - lc_c = 1/(lc_l * (2*pi*2*freq)**2) + lc_c = 1 / (lc_l * (2 * pi * 2 * freq) ** 2) return lc_l, c, lc_c - def __init__(self, frequency: FloatLike, src_resistance: FloatLike, src_reactance: FloatLike, - load_resistance: FloatLike, tolerance: FloatLike, - voltage: RangeLike, current: RangeLike): + def __init__( + self, + frequency: FloatLike, + src_resistance: FloatLike, + src_reactance: FloatLike, + load_resistance: FloatLike, + tolerance: FloatLike, + voltage: RangeLike, + current: RangeLike, + ): super().__init__() self.input = self.Port(Passive.empty(), [Input]) self.output = self.Port(Passive.empty(), [Output]) @@ -94,8 +102,9 @@ def __init__(self, frequency: FloatLike, src_resistance: FloatLike, src_reactanc self.current = self.ArgParameter(current) self.tolerance = self.ArgParameter(tolerance) - self.generator_param(self.frequency, self.src_resistance, self.src_reactance, self.load_resistance, - self.tolerance) + self.generator_param( + self.frequency, self.src_resistance, self.src_reactance, self.load_resistance, self.tolerance + ) @override def generate(self) -> None: @@ -107,9 +116,9 @@ def generate(self) -> None: l, c, c_lc = self._calculate_values(self.get(self.frequency), zs, rl) tolerance = self.get(self.tolerance) - self.l = self.Block(Inductor(inductance=l*Henry(tol=tolerance), current=self.current)) - self.c = self.Block(Capacitor(capacitance=c*Farad(tol=tolerance), voltage=self.voltage)) - self.c_lc = self.Block(Capacitor(capacitance=c_lc*Farad(tol=tolerance), voltage=self.voltage)) + self.l = self.Block(Inductor(inductance=l * Henry(tol=tolerance), current=self.current)) + self.c = self.Block(Capacitor(capacitance=c * Farad(tol=tolerance), voltage=self.voltage)) + self.c_lc = self.Block(Capacitor(capacitance=c_lc * Farad(tol=tolerance), voltage=self.voltage)) self.connect(self.input, self.l.a, self.c_lc.neg) self.connect(self.l.b, self.c_lc.pos, self.c.pos, self.output) @@ -136,13 +145,14 @@ def _calculate_values(cls, freq: float, z1: complex, z2: complex) -> Tuple[float q = sqrt(rp1 / rs2 - 1) net_xp = rp1 / q - net_xs = - q * rs2 # TODO: where is the negative sign coming from + net_xs = -q * rs2 # TODO: where is the negative sign coming from if xp1 != 0: - net_xp = 1/(1/net_xp - 1/xp1) # add reactance to cancel out z1 in parallel + net_xp = 1 / (1 / net_xp - 1 / xp1) # add reactance to cancel out z1 in parallel - return PiLowPassFilter._reactance_to_inductance(freq, net_xp), \ - PiLowPassFilter._reactance_to_capacitance(freq, net_xs - xs2) + return PiLowPassFilter._reactance_to_inductance(freq, net_xp), PiLowPassFilter._reactance_to_capacitance( + freq, net_xs - xs2 + ) class PiLowPassFilter(GeneratorBlock, RfFilter): @@ -155,29 +165,37 @@ class PiLowPassFilter(GeneratorBlock, RfFilter): WORK IN PROGRESS. NON-STABLE API. TODO: use ranges and tolerances throughout""" + @classmethod def _reactance_to_capacitance(cls, freq: float, reactance: float) -> float: - return -1 / (2*pi*freq*reactance) # negative reactance is capacitive + return -1 / (2 * pi * freq * reactance) # negative reactance is capacitive @classmethod def _reactance_to_inductance(cls, freq: float, reactance: float) -> float: - return reactance / (2*pi*freq) + return reactance / (2 * pi * freq) @classmethod def _calculate_values(cls, freq: float, q: float, z1: complex, z2: complex) -> Tuple[float, float, float, float]: """Given the center frequency, q factor, impedances z1 and z2, calculate the matching network and returns C1, C2, L, and virtual resistance Rv""" rh = max(z1.real, z2.real) - rv = rh / (q*q + 1) + rv = rh / (q * q + 1) l1, c1 = LLowPassFilter._calculate_values(freq, complex(rv, 0), z1) l2, c2 = LLowPassFilter._calculate_values(freq, complex(rv, 0), z2) return c1, c2, l1 + l2, rv - def __init__(self, frequency: RangeLike, src_resistance: FloatLike, src_reactance: FloatLike, - load_resistance: FloatLike, tolerance: FloatLike, - voltage: RangeLike, current: RangeLike): + def __init__( + self, + frequency: RangeLike, + src_resistance: FloatLike, + src_reactance: FloatLike, + load_resistance: FloatLike, + tolerance: FloatLike, + voltage: RangeLike, + current: RangeLike, + ): super().__init__() self.input = self.Port(Passive.empty(), [Input]) self.output = self.Port(Passive.empty(), [Output]) @@ -191,8 +209,9 @@ def __init__(self, frequency: RangeLike, src_resistance: FloatLike, src_reactanc self.current = self.ArgParameter(current) self.tolerance = self.ArgParameter(tolerance) - self.generator_param(self.frequency, self.src_resistance, self.src_reactance, self.load_resistance, - self.tolerance) + self.generator_param( + self.frequency, self.src_resistance, self.src_reactance, self.load_resistance, self.tolerance + ) @override def generate(self) -> None: @@ -209,9 +228,9 @@ def generate(self) -> None: tolerance = self.get(self.tolerance) - self.c1 = self.Block(Capacitor(capacitance=c1*Farad(tol=tolerance), voltage=self.voltage)) - self.c2 = self.Block(Capacitor(capacitance=c2*Farad(tol=tolerance), voltage=self.voltage)) - self.l = self.Block(Inductor(inductance=l*Henry(tol=tolerance), current=self.current)) + self.c1 = self.Block(Capacitor(capacitance=c1 * Farad(tol=tolerance), voltage=self.voltage)) + self.c2 = self.Block(Capacitor(capacitance=c2 * Farad(tol=tolerance), voltage=self.voltage)) + self.l = self.Block(Inductor(inductance=l * Henry(tol=tolerance), current=self.current)) self.connect(self.input, self.c1.pos, self.l.a) self.connect(self.l.b, self.c2.pos, self.output) self.connect(self.gnd, self.c1.neg.adapt_to(Ground()), self.c2.neg.adapt_to(Ground())) diff --git a/edg/abstract_parts/SelectorArea.py b/edg/abstract_parts/SelectorArea.py index 97e11263c..a0d8f338f 100644 --- a/edg/abstract_parts/SelectorArea.py +++ b/edg/abstract_parts/SelectorArea.py @@ -9,41 +9,44 @@ @abstract_block class SelectorArea(PartsTablePart): - """A base mixin that defines a footprint_area range specification for blocks that automatically select parts. - Provides no implementation, only defines the specification parameter. - - Some common areas for SMD parts: - 01005 R=0.72 C=0.72 D=0.72 - 0201 R=0.98 C=0.98 D=0.98 - 0402 R=1.7484 C=1.6744 D=1.7484 - 0603 R=4.3216 C=4.3216 D=4.3216 - 0805 R=6.384 C=6.664 D=6.384 - 1206 R=10.2144 C=10.58 D=10.2144 - 1812 R=23.01 C=23.4 D=23.01 - 2512 R=29.3376 D=29.3376 - """ - def __init__(self, *args: Any, footprint_area: RangeLike = RangeExpr.ALL, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.footprint_area = self.ArgParameter(footprint_area) - - @classmethod - def _footprint_area(cls, footprint_name: str) -> float: - return FootprintDataTable.area_of(footprint_name) + """A base mixin that defines a footprint_area range specification for blocks that automatically select parts. + Provides no implementation, only defines the specification parameter. + + Some common areas for SMD parts: + 01005 R=0.72 C=0.72 D=0.72 + 0201 R=0.98 C=0.98 D=0.98 + 0402 R=1.7484 C=1.6744 D=1.7484 + 0603 R=4.3216 C=4.3216 D=4.3216 + 0805 R=6.384 C=6.664 D=6.384 + 1206 R=10.2144 C=10.58 D=10.2144 + 1812 R=23.01 C=23.4 D=23.01 + 2512 R=29.3376 D=29.3376 + """ + + def __init__(self, *args: Any, footprint_area: RangeLike = RangeExpr.ALL, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.footprint_area = self.ArgParameter(footprint_area) + + @classmethod + def _footprint_area(cls, footprint_name: str) -> float: + return FootprintDataTable.area_of(footprint_name) @non_library class PartsTableAreaSelector(PartsTableFootprintFilter, SelectorArea): - """Defines an implementation for the area selector using parts tables and KICAD_FOOTPRINT.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.footprint_area) - - @override - def _row_filter(self, row: PartsTableRow) -> bool: - return super()._row_filter(row) and \ - (Range.exact(FootprintDataTable.area_of(row[self.KICAD_FOOTPRINT])).fuzzy_in(self.get(self.footprint_area))) - - @classmethod - def _row_area(cls, row: PartsTableRow) -> float: - """Returns the area of the part in the row, for use in sorting.""" - return FootprintDataTable.area_of(row[cls.KICAD_FOOTPRINT]) + """Defines an implementation for the area selector using parts tables and KICAD_FOOTPRINT.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.footprint_area) + + @override + def _row_filter(self, row: PartsTableRow) -> bool: + return super()._row_filter(row) and ( + Range.exact(FootprintDataTable.area_of(row[self.KICAD_FOOTPRINT])).fuzzy_in(self.get(self.footprint_area)) + ) + + @classmethod + def _row_area(cls, row: PartsTableRow) -> float: + """Returns the area of the part in the row, for use in sorting.""" + return FootprintDataTable.area_of(row[cls.KICAD_FOOTPRINT]) diff --git a/edg/abstract_parts/StandardFootprint.py b/edg/abstract_parts/StandardFootprint.py index 5eb34fd4b..12337cb77 100644 --- a/edg/abstract_parts/StandardFootprint.py +++ b/edg/abstract_parts/StandardFootprint.py @@ -4,50 +4,57 @@ from ..electronics_model import * -StandardPinningType = TypeVar('StandardPinningType', bound=Block) +StandardPinningType = TypeVar("StandardPinningType", bound=Block) PinningFunction = Callable[[StandardPinningType], Dict[str, CircuitPort]] -class StandardFootprint(Generic[StandardPinningType]): - """A shared helper class that provides a table to provide standard pin mapping from footprints.""" - REFDES_PREFIX: ClassVar[str] - FOOTPRINT_PINNING_MAP: ClassVar[Dict[Union[str, Tuple[str, ...]], PinningFunction[StandardPinningType]]] # user-specified - _EXPANDED_FOOTPRINT_PINNING_MAP: Optional[Dict[str, PinningFunction[StandardPinningType]]] = None # automatically-generated from above - - @classmethod - def _footprint_pinning_map(cls) -> Dict[str, PinningFunction[StandardPinningType]]: - """Returns the footprint pinning map as a dict of footprint name -> pinning fn, generating and caching the - expanded table as needed""" - if cls._EXPANDED_FOOTPRINT_PINNING_MAP is None: - footprint_map = {} - for pinning_footprints, pinning_fn in cls.FOOTPRINT_PINNING_MAP.items(): - if isinstance(pinning_footprints, tuple): - for pinning_footprint in pinning_footprints: - assert pinning_footprint not in footprint_map, f"duplicate footprint entry {pinning_footprint}" - footprint_map[pinning_footprint] = pinning_fn - elif isinstance(pinning_footprints, str): - assert pinning_footprints not in footprint_map, f"duplicate footprint entry {pinning_footprints}" - footprint_map[pinning_footprints] = pinning_fn - else: - raise ValueError(f"unknown footprint entry {pinning_footprints}") - cls._EXPANDED_FOOTPRINT_PINNING_MAP = footprint_map - return cls._EXPANDED_FOOTPRINT_PINNING_MAP - @classmethod - def _make_pinning(cls, block: StandardPinningType, footprint: str) -> Dict[str, CircuitPort]: - """Returns the pinning for a footprint for a specific block's pins""" - return cls._footprint_pinning_map()[footprint](block) + +class StandardFootprint(Generic[StandardPinningType]): + """A shared helper class that provides a table to provide standard pin mapping from footprints.""" + + REFDES_PREFIX: ClassVar[str] + FOOTPRINT_PINNING_MAP: ClassVar[ + Dict[Union[str, Tuple[str, ...]], PinningFunction[StandardPinningType]] + ] # user-specified + _EXPANDED_FOOTPRINT_PINNING_MAP: Optional[Dict[str, PinningFunction[StandardPinningType]]] = ( + None # automatically-generated from above + ) + + @classmethod + def _footprint_pinning_map(cls) -> Dict[str, PinningFunction[StandardPinningType]]: + """Returns the footprint pinning map as a dict of footprint name -> pinning fn, generating and caching the + expanded table as needed""" + if cls._EXPANDED_FOOTPRINT_PINNING_MAP is None: + footprint_map = {} + for pinning_footprints, pinning_fn in cls.FOOTPRINT_PINNING_MAP.items(): + if isinstance(pinning_footprints, tuple): + for pinning_footprint in pinning_footprints: + assert pinning_footprint not in footprint_map, f"duplicate footprint entry {pinning_footprint}" + footprint_map[pinning_footprint] = pinning_fn + elif isinstance(pinning_footprints, str): + assert pinning_footprints not in footprint_map, f"duplicate footprint entry {pinning_footprints}" + footprint_map[pinning_footprints] = pinning_fn + else: + raise ValueError(f"unknown footprint entry {pinning_footprints}") + cls._EXPANDED_FOOTPRINT_PINNING_MAP = footprint_map + return cls._EXPANDED_FOOTPRINT_PINNING_MAP + + @classmethod + def _make_pinning(cls, block: StandardPinningType, footprint: str) -> Dict[str, CircuitPort]: + """Returns the pinning for a footprint for a specific block's pins""" + return cls._footprint_pinning_map()[footprint](block) @non_library class HasStandardFootprint(Block): - """Base class that defines that a class supports a StandardFootprint""" - # the footprint can be a lambda to allow the pinning to be defined after - _STANDARD_FOOTPRINT: ClassVar[Union[Type[StandardFootprint[Self]], - Callable[[], Type[StandardFootprint[Self]]]]] - - @classmethod - def _standard_footprint(cls) -> Type[StandardFootprint[Self]]: - """Returns the StandardFootprint class for this block""" - if callable(cls._STANDARD_FOOTPRINT): - return cls._STANDARD_FOOTPRINT() # type: ignore - else: - return cls._STANDARD_FOOTPRINT + """Base class that defines that a class supports a StandardFootprint""" + + # the footprint can be a lambda to allow the pinning to be defined after + _STANDARD_FOOTPRINT: ClassVar[Union[Type[StandardFootprint[Self]], Callable[[], Type[StandardFootprint[Self]]]]] + + @classmethod + def _standard_footprint(cls) -> Type[StandardFootprint[Self]]: + """Returns the StandardFootprint class for this block""" + if callable(cls._STANDARD_FOOTPRINT): + return cls._STANDARD_FOOTPRINT() # type: ignore + else: + return cls._STANDARD_FOOTPRINT diff --git a/edg/abstract_parts/TouchPad.py b/edg/abstract_parts/TouchPad.py index f7cac2adb..a750e9396 100644 --- a/edg/abstract_parts/TouchPad.py +++ b/edg/abstract_parts/TouchPad.py @@ -13,4 +13,4 @@ def __init__(self, touch_footprint: StringLike): @override def contents(self) -> None: super().contents() - self.footprint('U', self.touch_footprint, {'1': self.pad}) + self.footprint("U", self.touch_footprint, {"1": self.pad}) diff --git a/edg/abstract_parts/UsbBitBang.py b/edg/abstract_parts/UsbBitBang.py index 3004b7a6f..3cec99ee7 100644 --- a/edg/abstract_parts/UsbBitBang.py +++ b/edg/abstract_parts/UsbBitBang.py @@ -8,68 +8,77 @@ class UsbBitBang(BitBangAdapter, Block): - """Bit-bang circuit for USB, from the UPduino3.0 circuit and for 3.3v. - Presumably generalizes to any digital pin that can be driven fast enough. + """Bit-bang circuit for USB, from the UPduino3.0 circuit and for 3.3v. + Presumably generalizes to any digital pin that can be driven fast enough. - TODO: a more formal analysis of tolerances""" - @staticmethod - def digital_external_from_link(link_port: DigitalBidir) -> DigitalBidir: - """Creates a DigitalBidir model that is the external-facing port that exports from - an internal-facing (link-side) port. The internal-facing port should be ideal. - These are basically the semantics of a DigitalBidir bridge. - TODO: unify code w/ DigitalBidir bridge?""" - return DigitalBidir( - voltage_out=link_port.link().voltage, current_draw=link_port.link().current_drawn, - voltage_limits=link_port.link().voltage_limits, current_limits=link_port.link().current_limits, - output_thresholds=link_port.link().output_thresholds, input_thresholds=link_port.link().input_thresholds, - pulldown_capable=link_port.link().pulldown_capable, pullup_capable=link_port.link().pullup_capable - ) + TODO: a more formal analysis of tolerances""" - def __init__(self) -> None: - super().__init__() - self.usb = self.Port(UsbDevicePort.empty(), [Output]) + @staticmethod + def digital_external_from_link(link_port: DigitalBidir) -> DigitalBidir: + """Creates a DigitalBidir model that is the external-facing port that exports from + an internal-facing (link-side) port. The internal-facing port should be ideal. + These are basically the semantics of a DigitalBidir bridge. + TODO: unify code w/ DigitalBidir bridge?""" + return DigitalBidir( + voltage_out=link_port.link().voltage, + current_draw=link_port.link().current_drawn, + voltage_limits=link_port.link().voltage_limits, + current_limits=link_port.link().current_limits, + output_thresholds=link_port.link().output_thresholds, + input_thresholds=link_port.link().input_thresholds, + pulldown_capable=link_port.link().pulldown_capable, + pullup_capable=link_port.link().pullup_capable, + ) - # Internally, this behaves like a bridge, with defined 'external' (USB) and 'internal' (FPGA) - # sides and propagating port data from internal to external as with bridge semantics. - # Undirected / bidirectional propagation doesn't work with the current solver, since - # we need the FPGA-side link voltage to propagate to the USB port, and the USB-side link voltage - # to propagate to the FPGA port, and this causes both to deadlock (both link voltages depend on - # the port voltages, and neither is available until the other link voltage is available). - # Other ideas include moving to a fixed point solver, but that has other trade-offs. - self.dp = self.Port(DigitalBidir.empty()) - self.dm = self.Port(DigitalBidir.empty()) - self.dp_pull = self.Port(DigitalSink.empty()) + def __init__(self) -> None: + super().__init__() + self.usb = self.Port(UsbDevicePort.empty(), [Output]) - @override - def contents(self) -> None: - super().contents() + # Internally, this behaves like a bridge, with defined 'external' (USB) and 'internal' (FPGA) + # sides and propagating port data from internal to external as with bridge semantics. + # Undirected / bidirectional propagation doesn't work with the current solver, since + # we need the FPGA-side link voltage to propagate to the USB port, and the USB-side link voltage + # to propagate to the FPGA port, and this causes both to deadlock (both link voltages depend on + # the port voltages, and neither is available until the other link voltage is available). + # Other ideas include moving to a fixed point solver, but that has other trade-offs. + self.dp = self.Port(DigitalBidir.empty()) + self.dm = self.Port(DigitalBidir.empty()) + self.dp_pull = self.Port(DigitalSink.empty()) - self.dp_pull_res = self.Block(Resistor(1.5*kOhm(tol=0.05))) + @override + def contents(self) -> None: + super().contents() - self.dp_res = self.Block(Resistor(68*Ohm(tol=0.05))) - self.dm_res = self.Block(Resistor(68*Ohm(tol=0.05))) + self.dp_pull_res = self.Block(Resistor(1.5 * kOhm(tol=0.05))) - self.connect(self.dm, self.dm_res.a.adapt_to(DigitalBidir())) # internal ports are ideal - self.connect(self.usb.dm, self.dm_res.b.adapt_to( - self.digital_external_from_link(self.dm))) + self.dp_res = self.Block(Resistor(68 * Ohm(tol=0.05))) + self.dm_res = self.Block(Resistor(68 * Ohm(tol=0.05))) - self.connect(self.dp, self.dp_res.a.adapt_to(DigitalBidir())) - self.connect(self.usb.dp, self.dp_res.b.adapt_to(DigitalBidir( - voltage_out=self.dp.link().voltage.hull(self.dp_pull.link().voltage), - current_draw=self.dp.link().current_drawn + self.dp_pull.link().current_drawn, - voltage_limits=self.dp.link().voltage_limits.intersect(self.dp_pull.link().voltage_limits), - current_limits=self.dp.link().current_limits.intersect(self.dp_pull.link().current_limits), - output_thresholds=self.dp.link().output_thresholds.intersect(self.dp_pull.link().output_thresholds), - input_thresholds=self.dp.link().input_thresholds.hull(self.dp_pull.link().input_thresholds), - pulldown_capable=self.dp.link().pulldown_capable | self.dp_pull.link().pulldown_capable, - pullup_capable=self.dp.link().pullup_capable | self.dp_pull.link().pullup_capable - ))) + self.connect(self.dm, self.dm_res.a.adapt_to(DigitalBidir())) # internal ports are ideal + self.connect(self.usb.dm, self.dm_res.b.adapt_to(self.digital_external_from_link(self.dm))) - self.connect(self.dp_pull, self.dp_pull_res.a.adapt_to(DigitalSink())) - self.connect(self.dp_pull_res.b, self.dp_res.b) # upstream of adapter + self.connect(self.dp, self.dp_res.a.adapt_to(DigitalBidir())) + self.connect( + self.usb.dp, + self.dp_res.b.adapt_to( + DigitalBidir( + voltage_out=self.dp.link().voltage.hull(self.dp_pull.link().voltage), + current_draw=self.dp.link().current_drawn + self.dp_pull.link().current_drawn, + voltage_limits=self.dp.link().voltage_limits.intersect(self.dp_pull.link().voltage_limits), + current_limits=self.dp.link().current_limits.intersect(self.dp_pull.link().current_limits), + output_thresholds=self.dp.link().output_thresholds.intersect(self.dp_pull.link().output_thresholds), + input_thresholds=self.dp.link().input_thresholds.hull(self.dp_pull.link().input_thresholds), + pulldown_capable=self.dp.link().pulldown_capable | self.dp_pull.link().pulldown_capable, + pullup_capable=self.dp.link().pullup_capable | self.dp_pull.link().pullup_capable, + ) + ), + ) - def connected_from(self, dp_pull: Port[DigitalLink], dp: Port[DigitalLink], dm: Port[DigitalLink]) -> 'UsbBitBang': - cast(Block, builder.get_enclosing_block()).connect(dp_pull, self.dp_pull) - cast(Block, builder.get_enclosing_block()).connect(dp, self.dp) - cast(Block, builder.get_enclosing_block()).connect(dm, self.dm) - return self + self.connect(self.dp_pull, self.dp_pull_res.a.adapt_to(DigitalSink())) + self.connect(self.dp_pull_res.b, self.dp_res.b) # upstream of adapter + + def connected_from(self, dp_pull: Port[DigitalLink], dp: Port[DigitalLink], dm: Port[DigitalLink]) -> "UsbBitBang": + cast(Block, builder.get_enclosing_block()).connect(dp_pull, self.dp_pull) + cast(Block, builder.get_enclosing_block()).connect(dp, self.dp) + cast(Block, builder.get_enclosing_block()).connect(dm, self.dm) + return self diff --git a/edg/abstract_parts/UsbConnectors.py b/edg/abstract_parts/UsbConnectors.py index ea07e3c12..7eb5e2260 100644 --- a/edg/abstract_parts/UsbConnectors.py +++ b/edg/abstract_parts/UsbConnectors.py @@ -5,36 +5,39 @@ @abstract_block @non_library class UsbConnector(Connector): - """USB connector of any generation / type.""" - USB2_VOLTAGE_RANGE = (4.75, 5.25)*Volt - USB2_CURRENT_LIMITS = (0, 0.5)*Amp + """USB connector of any generation / type.""" + + USB2_VOLTAGE_RANGE = (4.75, 5.25) * Volt + USB2_CURRENT_LIMITS = (0, 0.5) * Amp @abstract_block class UsbHostConnector(UsbConnector): - """Abstract base class for a USB 2.0 device-side port connector""" - def __init__(self) -> None: - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), optional=True) - self.gnd = self.Port(Ground.empty()) + """Abstract base class for a USB 2.0 device-side port connector""" + + def __init__(self) -> None: + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), optional=True) + self.gnd = self.Port(Ground.empty()) - self.usb = self.Port(UsbDevicePort.empty(), optional=True) + self.usb = self.Port(UsbDevicePort.empty(), optional=True) @abstract_block class UsbDeviceConnector(UsbConnector, PowerSource): - """Abstract base class for a USB 2.0 device-side port connector""" - def __init__(self) -> None: - super().__init__() - self.pwr = self.Port(VoltageSource.empty(), optional=True) - self.gnd = self.Port(Ground.empty()) + """Abstract base class for a USB 2.0 device-side port connector""" + + def __init__(self) -> None: + super().__init__() + self.pwr = self.Port(VoltageSource.empty(), optional=True) + self.gnd = self.Port(Ground.empty()) - self.usb = self.Port(UsbHostPort.empty(), optional=True) + self.usb = self.Port(UsbHostPort.empty(), optional=True) @abstract_block class UsbEsdDiode(Protection): - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground.empty(), [Common]) - self.usb = self.Port(UsbPassivePort.empty(), [InOut]) + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground.empty(), [Common]) + self.usb = self.Port(UsbPassivePort.empty(), [InOut]) diff --git a/edg/abstract_parts/VariantPinRemapper.py b/edg/abstract_parts/VariantPinRemapper.py index 33997fda9..61b1bdb3f 100644 --- a/edg/abstract_parts/VariantPinRemapper.py +++ b/edg/abstract_parts/VariantPinRemapper.py @@ -4,26 +4,26 @@ class VariantPinRemapper: - def __init__(self, mapping: Dict[str, CircuitPort]): - self.mapping = mapping + def __init__(self, mapping: Dict[str, CircuitPort]): + self.mapping = mapping - def remap(self, remap: Dict[str, Union[str, List[str]]]) -> Dict[str, CircuitPort]: - output_dict: Dict[str, CircuitPort] = {} - remapped_names: Set[str] = set() - for (orig_name, orig_port) in self.mapping.items(): - assert orig_name in remap, f"missing remap rule for {orig_name}" - remapped_names.add(orig_name) - remapping = remap[orig_name] - if isinstance(remapping, str): - assert remapping not in output_dict, f"duplicate remap to {remapping}" - output_dict[remapping] = orig_port - elif isinstance(remapping, list): - for remap_name in remapping: - assert remap_name not in output_dict, f"duplicate remap to {remap_name}" - output_dict[remap_name] = orig_port - else: - raise NotImplementedError(f"unknown remap rule {remap[orig_name]} for {orig_name}") - missed_names = set(self.mapping.keys()).difference(remapped_names) - assert not missed_names, f"pins not remapped: {missed_names}" + def remap(self, remap: Dict[str, Union[str, List[str]]]) -> Dict[str, CircuitPort]: + output_dict: Dict[str, CircuitPort] = {} + remapped_names: Set[str] = set() + for orig_name, orig_port in self.mapping.items(): + assert orig_name in remap, f"missing remap rule for {orig_name}" + remapped_names.add(orig_name) + remapping = remap[orig_name] + if isinstance(remapping, str): + assert remapping not in output_dict, f"duplicate remap to {remapping}" + output_dict[remapping] = orig_port + elif isinstance(remapping, list): + for remap_name in remapping: + assert remap_name not in output_dict, f"duplicate remap to {remap_name}" + output_dict[remap_name] = orig_port + else: + raise NotImplementedError(f"unknown remap rule {remap[orig_name]} for {orig_name}") + missed_names = set(self.mapping.keys()).difference(remapped_names) + assert not missed_names, f"pins not remapped: {missed_names}" - return output_dict + return output_dict diff --git a/edg/abstract_parts/__init__.py b/edg/abstract_parts/__init__.py index 2e7180fa6..1c673ec2c 100644 --- a/edg/abstract_parts/__init__.py +++ b/edg/abstract_parts/__init__.py @@ -2,8 +2,14 @@ from ..electronics_model import * from .PartsTable import PartsTable, PartsTableColumn, PartsTableRow -from .PartsTablePart import PartsTableBase, PartsTablePart, SelectorFootprint, PartsTableSelector,\ - PartsTableFootprintFilter, PartsTableSelectorFootprint +from .PartsTablePart import ( + PartsTableBase, + PartsTablePart, + SelectorFootprint, + PartsTableSelector, + PartsTableFootprintFilter, + PartsTableSelectorFootprint, +) from .Categories import DummyDevice from .Categories import DiscreteComponent, DiscreteSemiconductor, PassiveComponent @@ -15,8 +21,19 @@ from .Categories import PowerConditioner, PowerSwitch, MotorDriver, BrushedMotorDriver, BldcDriver from .Categories import PowerSource, Connector, ProgrammingConnector from .Categories import HumanInterface, Display, Lcd, Oled, EInk, Light -from .Categories import Sensor, CurrentSensor, Accelerometer, Gyroscope, MagneticSensor, MagneticSwitch, Magnetometer,\ - DistanceSensor, Microphone, Camera, LightSensor +from .Categories import ( + Sensor, + CurrentSensor, + Accelerometer, + Gyroscope, + MagneticSensor, + MagneticSwitch, + Magnetometer, + DistanceSensor, + Microphone, + Camera, + LightSensor, +) from .Categories import EnvironmentalSensor, TemperatureSensor, HumiditySensor, PressureSensor, GasSensor from .Categories import Label, Testing, TypedJumper, TypedTestPoint, InternalSubcircuit, DeprecatedBlock, Mechanical from .Categories import MultipackDevice @@ -25,21 +42,44 @@ from .SelectorArea import SelectorArea, PartsTableAreaSelector from .AbstractDevices import Battery -from .AbstractConnector import BananaJack, BananaSafetyJack, RfConnector, RfConnectorTestPoint, RfConnectorAntenna,\ - UflConnector, SmaConnector, SmaMConnector, SmaFConnector +from .AbstractConnector import ( + BananaJack, + BananaSafetyJack, + RfConnector, + RfConnectorTestPoint, + RfConnectorAntenna, + UflConnector, + SmaConnector, + SmaMConnector, + SmaFConnector, +) from .AbstractResistor import Resistor, ResistorStandardFootprint, TableResistor, SeriesResistor from .AbstractResistor import PulldownResistor, PullupResistor, PulldownResistorArray, PullupResistorArray from .AbstractResistor import SeriesPowerResistor, CurrentSenseResistor, AnalogClampResistor, DigitalClampResistor from .AbstractResistorArray import ResistorArray, ResistorArrayStandardFootprint, TableResistorArray -from .AbstractCapacitor import UnpolarizedCapacitor, Capacitor, CeramicCapacitor, AluminumCapacitor, \ - CapacitorStandardFootprint, TableCapacitor, TableDeratingCapacitor +from .AbstractCapacitor import ( + UnpolarizedCapacitor, + Capacitor, + CeramicCapacitor, + AluminumCapacitor, + CapacitorStandardFootprint, + TableCapacitor, + TableDeratingCapacitor, +) from .AbstractCapacitor import DummyCapacitorFootprint, DecouplingCapacitor, AnalogCapacitor, CombinedCapacitor from .AbstractInductor import Inductor, TableInductor, SeriesPowerInductor from .AbstractFerriteBead import FerriteBead, FerriteBeadStandardFootprint, TableFerriteBead, SeriesPowerFerriteBead from .ResistiveDivider import ResistiveDivider, VoltageDivider, VoltageSenseDivider from .ResistiveDivider import FeedbackVoltageDivider, SignalDivider -from .PassiveFilters import LowPassRc, AnalogLowPassRc, DigitalLowPassRc, DigitalLowPassRcArray, LowPassRcDac, \ - PullupDelayRc, LowPassAnalogDifferentialRc +from .PassiveFilters import ( + LowPassRc, + AnalogLowPassRc, + DigitalLowPassRc, + DigitalLowPassRcArray, + LowPassRcDac, + PullupDelayRc, + LowPassAnalogDifferentialRc, +) from .RfNetworks import DiscreteRfWarning, LLowPassFilter, LHighPassFilter, LLowPassFilterWith2HNotch, PiLowPassFilter from .I2cPullup import I2cPullup from .LevelShifter import BidirectionaLevelShifter @@ -49,7 +89,13 @@ from .AbstractDiodes import ZenerDiode, TableZenerDiode, ProtectionZenerDiode, AnalogClampZenerDiode from .AbstractTvsDiode import TvsDiode, ProtectionTvsDiode, DigitalTvsDiode from .AbstractLed import Led, LedStandardFootprint, TableLed, RgbLedCommonAnode, LedColor, LedColorLike -from .AbstractLed import IndicatorLed, IndicatorSinkLed, IndicatorSinkLedResistor, VoltageIndicatorLed, IndicatorSinkRgbLed +from .AbstractLed import ( + IndicatorLed, + IndicatorSinkLed, + IndicatorSinkLedResistor, + VoltageIndicatorLed, + IndicatorSinkRgbLed, +) from .AbstractLed import IndicatorSinkPackedRgbLed from .AbstractLed import IndicatorLedArray, IndicatorSinkLedArray from .AbstractBjt import Bjt, BjtStandardFootprint, TableBjt @@ -72,16 +118,37 @@ from .AbstractPowerConverters import LinearRegulator, VoltageReference, LinearRegulatorDevice, SwitchingVoltageRegulator from .AbstractPowerConverters import BuckConverter, DiscreteBuckConverter, BoostConverter, DiscreteBoostConverter from .AbstractPowerConverters import BuckConverterPowerPath, BoostConverterPowerPath, BuckBoostConverterPowerPath -from .PowerCircuits import HalfBridge, FetHalfBridge, HalfBridgeIndependent, HalfBridgePwm, FetHalfBridgeIndependent,\ - FetHalfBridgePwmReset, RampLimiter +from .PowerCircuits import ( + HalfBridge, + FetHalfBridge, + HalfBridgeIndependent, + HalfBridgePwm, + FetHalfBridgeIndependent, + FetHalfBridgePwmReset, + RampLimiter, +) from .AbstractLedDriver import LedDriver, LedDriverPwm, LedDriverSwitchingConverter from .AbstractFuse import Fuse, SeriesPowerFuse, PptcFuse, FuseStandardFootprint, TableFuse, SeriesPowerPptcFuse from .AbstractCrystal import Crystal, TableCrystal, OscillatorReference, CeramicResonator from .AbstractOscillator import Oscillator, TableOscillator -from .AbstractDebugHeaders import SwdCortexTargetConnector, SwdCortexTargetConnectorReset, \ - SwdCortexTargetConnectorSwo, SwdCortexTargetConnectorTdi -from .AbstractTestPoint import TestPoint, GroundTestPoint, VoltageTestPoint, DigitalTestPoint, DigitalArrayTestPoint, \ - AnalogTestPoint, I2cTestPoint, SpiTestPoint, CanControllerTestPoint, CanDiffTestPoint +from .AbstractDebugHeaders import ( + SwdCortexTargetConnector, + SwdCortexTargetConnectorReset, + SwdCortexTargetConnectorSwo, + SwdCortexTargetConnectorTdi, +) +from .AbstractTestPoint import ( + TestPoint, + GroundTestPoint, + VoltageTestPoint, + DigitalTestPoint, + DigitalArrayTestPoint, + AnalogTestPoint, + I2cTestPoint, + SpiTestPoint, + CanControllerTestPoint, + CanDiffTestPoint, +) from .AbstractTestPoint import AnalogCoaxTestPoint from .AbstractJumper import Jumper, DigitalJumper from .PassiveConnector import PassiveConnector, FootprintPassiveConnector @@ -97,8 +164,17 @@ from .IoController import BaseIoController, IoController, IoControllerPowerRequired, BaseIoControllerPinmapGenerator from .IoControllerExportable import BaseIoControllerExportable -from .IoControllerInterfaceMixins import IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerTouchDriver,\ - IoControllerDac, IoControllerCan, IoControllerUsb, IoControllerI2s, IoControllerDvp8, IoControllerUsbCc +from .IoControllerInterfaceMixins import ( + IoControllerSpiPeripheral, + IoControllerI2cTarget, + IoControllerTouchDriver, + IoControllerDac, + IoControllerCan, + IoControllerUsb, + IoControllerI2s, + IoControllerDvp8, + IoControllerUsbCc, +) from .IoControllerInterfaceMixins import IoControllerPowerOut, IoControllerUsbOut, IoControllerVin from .IoControllerInterfaceMixins import IoControllerWifi, IoControllerBluetooth, IoControllerBle from .IoControllerProgramming import IoControllerWithSwdTargetConnector @@ -112,10 +188,25 @@ from .GenericResistor import ESeriesResistor, GenericChipResistor, GenericAxialResistor, GenericAxialVerticalResistor from .GenericCapacitor import GenericMlcc -from .DummyDevices import DummyPassive, DummyGround, DummyVoltageSource, DummyVoltageSink, DummyDigitalSource, \ - DummyDigitalSink, DummyAnalogSource, DummyAnalogSink -from .DummyDevices import ForcedVoltageCurrentDraw, ForcedVoltageCurrentLimit, ForcedVoltage, ForcedVoltageCurrent, \ - ForcedAnalogVoltage, ForcedAnalogSignal, ForcedDigitalSinkCurrentDraw +from .DummyDevices import ( + DummyPassive, + DummyGround, + DummyVoltageSource, + DummyVoltageSink, + DummyDigitalSource, + DummyDigitalSink, + DummyAnalogSource, + DummyAnalogSink, +) +from .DummyDevices import ( + ForcedVoltageCurrentDraw, + ForcedVoltageCurrentLimit, + ForcedVoltage, + ForcedVoltageCurrent, + ForcedAnalogVoltage, + ForcedAnalogSignal, + ForcedDigitalSinkCurrentDraw, +) from .MergedBlocks import MergedVoltageSource, MergedDigitalSource, MergedAnalogSource, MergedSpiController from .Nonstrict3v3Compatible import Nonstrict3v3Compatible diff --git a/edg/abstract_parts/test_capacitor_generic.py b/edg/abstract_parts/test_capacitor_generic.py index bdcaa2f21..5cd68747e 100644 --- a/edg/abstract_parts/test_capacitor_generic.py +++ b/edg/abstract_parts/test_capacitor_generic.py @@ -4,139 +4,131 @@ class CapacitorGenericTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericMlcc( - capacitance=0.1 * uFarad(tol=0.2), - voltage=(0, 3.3) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(GenericMlcc(capacitance=0.1 * uFarad(tol=0.2), voltage=(0, 3.3) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) class BigCapacitorGenericTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericMlcc( - capacitance=(50, 1000) * uFarad, - voltage=(0, 5) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(GenericMlcc(capacitance=(50, 1000) * uFarad, voltage=(0, 5) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) class HighVoltageCapacitorGenericTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericMlcc( - capacitance=0.2 * uFarad(tol=0.2), - voltage=(0, 20) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(GenericMlcc(capacitance=0.2 * uFarad(tol=0.2), voltage=(0, 20) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) class HighSingleCapacitorGenericTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericMlcc( - capacitance=22 * uFarad(tol=0.2), - voltage=(0, 10) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(GenericMlcc(capacitance=22 * uFarad(tol=0.2), voltage=(0, 10) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) class MediumSingleCapacitorGenericTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericMlcc( - capacitance=2 * uFarad(tol=0.2), - voltage=(0, 20) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(GenericMlcc(capacitance=2 * uFarad(tol=0.2), voltage=(0, 20) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + class DeratedCapacitorGenericTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericMlcc( - capacitance=1 * uFarad(tol=0.2), - voltage=(0, 5) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(GenericMlcc(capacitance=1 * uFarad(tol=0.2), voltage=(0, 5) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + class BigMultiCapacitorGenericTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericMlcc( - capacitance=(50, 1000) * uFarad, - voltage=(0, 5) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(GenericMlcc(capacitance=(50, 1000) * uFarad, voltage=(0, 5) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) class CapacitorTestCase(unittest.TestCase): - def test_capacitor(self) -> None: - compiled = ScalaCompiler.compile(CapacitorGenericTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_0402_1005Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '100nF') - - def test_capacitor_footprint(self) -> None: - compiled = ScalaCompiler.compile(CapacitorGenericTestTop, Refinements( - instance_values=[(['dut', 'footprint_spec'], 'Capacitor_SMD:C_1206_3216Metric')] - )) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '100nF') - - def test_multi_capacitor(self) -> None: - compiled = ScalaCompiler.compile(BigCapacitorGenericTestTop) - self.assertEqual(compiled.get_value(['dut', 'c[0]', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'c[0]', 'fp_value']), '22uF') - self.assertEqual(compiled.get_value(['dut', 'c[1]', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'c[1]', 'fp_value']), '22uF') - self.assertEqual(compiled.get_value(['dut', 'c[2]', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'c[2]', 'fp_value']), '22uF') - self.assertEqual(compiled.get_value(['dut', 'c[3]', 'fp_footprint']), None) - - def test_high_voltage_capacitor(self) -> None: - compiled = ScalaCompiler.compile(HighVoltageCapacitorGenericTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_0603_1608Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '220nF') - - def test_high_single_capacitor(self) -> None: - compiled = ScalaCompiler.compile(HighSingleCapacitorGenericTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '22uF') - - def test_medium_single_capacitor(self) -> None: - compiled = ScalaCompiler.compile(MediumSingleCapacitorGenericTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_0805_2012Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '2.2uF') - - def test_derated_capacitor(self) -> None: - compiled = ScalaCompiler.compile(DeratedCapacitorGenericTestTop, Refinements( - instance_values=[(['dut', 'footprint_spec'], 'Capacitor_SMD:C_1206_3216Metric'), - (['dut', 'derating_coeff'], 0.5),] - )) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '2.2uF') - - def test_derated_multi_capacitor(self) -> None: - compiled = ScalaCompiler.compile(BigMultiCapacitorGenericTestTop, Refinements( - instance_values=[(['dut', 'footprint_spec'], 'Capacitor_SMD:C_1206_3216Metric'), - (['dut', 'derating_coeff'], 0.5),] - )) - self.assertEqual(compiled.get_value(['dut', 'c[0]', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'c[0]', 'fp_value']), '22uF') - self.assertEqual(compiled.get_value(['dut', 'c[1]', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'c[1]', 'fp_value']), '22uF') - self.assertEqual(compiled.get_value(['dut', 'c[2]', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'c[2]', 'fp_value']), '22uF') - self.assertEqual(compiled.get_value(['dut', 'c[3]', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'c[3]', 'fp_value']), '22uF') - self.assertEqual(compiled.get_value(['dut', 'c[4]', 'fp_footprint']), 'Capacitor_SMD:C_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'c[4]', 'fp_value']), '22uF') - self.assertEqual(compiled.get_value(['dut', 'c[5]', 'fp_footprint']), None) + def test_capacitor(self) -> None: + compiled = ScalaCompiler.compile(CapacitorGenericTestTop) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_0402_1005Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "100nF") + + def test_capacitor_footprint(self) -> None: + compiled = ScalaCompiler.compile( + CapacitorGenericTestTop, + Refinements(instance_values=[(["dut", "footprint_spec"], "Capacitor_SMD:C_1206_3216Metric")]), + ) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "100nF") + + def test_multi_capacitor(self) -> None: + compiled = ScalaCompiler.compile(BigCapacitorGenericTestTop) + self.assertEqual(compiled.get_value(["dut", "c[0]", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "c[0]", "fp_value"]), "22uF") + self.assertEqual(compiled.get_value(["dut", "c[1]", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "c[1]", "fp_value"]), "22uF") + self.assertEqual(compiled.get_value(["dut", "c[2]", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "c[2]", "fp_value"]), "22uF") + self.assertEqual(compiled.get_value(["dut", "c[3]", "fp_footprint"]), None) + + def test_high_voltage_capacitor(self) -> None: + compiled = ScalaCompiler.compile(HighVoltageCapacitorGenericTestTop) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_0603_1608Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "220nF") + + def test_high_single_capacitor(self) -> None: + compiled = ScalaCompiler.compile(HighSingleCapacitorGenericTestTop) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "22uF") + + def test_medium_single_capacitor(self) -> None: + compiled = ScalaCompiler.compile(MediumSingleCapacitorGenericTestTop) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_0805_2012Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "2.2uF") + + def test_derated_capacitor(self) -> None: + compiled = ScalaCompiler.compile( + DeratedCapacitorGenericTestTop, + Refinements( + instance_values=[ + (["dut", "footprint_spec"], "Capacitor_SMD:C_1206_3216Metric"), + (["dut", "derating_coeff"], 0.5), + ] + ), + ) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "2.2uF") + + def test_derated_multi_capacitor(self) -> None: + compiled = ScalaCompiler.compile( + BigMultiCapacitorGenericTestTop, + Refinements( + instance_values=[ + (["dut", "footprint_spec"], "Capacitor_SMD:C_1206_3216Metric"), + (["dut", "derating_coeff"], 0.5), + ] + ), + ) + self.assertEqual(compiled.get_value(["dut", "c[0]", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "c[0]", "fp_value"]), "22uF") + self.assertEqual(compiled.get_value(["dut", "c[1]", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "c[1]", "fp_value"]), "22uF") + self.assertEqual(compiled.get_value(["dut", "c[2]", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "c[2]", "fp_value"]), "22uF") + self.assertEqual(compiled.get_value(["dut", "c[3]", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "c[3]", "fp_value"]), "22uF") + self.assertEqual(compiled.get_value(["dut", "c[4]", "fp_footprint"]), "Capacitor_SMD:C_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "c[4]", "fp_value"]), "22uF") + self.assertEqual(compiled.get_value(["dut", "c[5]", "fp_footprint"]), None) diff --git a/edg/abstract_parts/test_e_series.py b/edg/abstract_parts/test_e_series.py index 6047bed2a..da0bb9353 100644 --- a/edg/abstract_parts/test_e_series.py +++ b/edg/abstract_parts/test_e_series.py @@ -5,65 +5,71 @@ class PreferredNumberTestCase(unittest.TestCase): - def test_zigzag_range(self) -> None: - self.assertEqual(ESeriesUtil.zigzag_range(0, 1), [0]) - self.assertEqual(ESeriesUtil.zigzag_range(0, 2), [0, 1]) - self.assertEqual(ESeriesUtil.zigzag_range(0, 3), [1, 0, 2]) - self.assertEqual(ESeriesUtil.zigzag_range(0, 4), [1, 0, 2, 3]) + def test_zigzag_range(self) -> None: + self.assertEqual(ESeriesUtil.zigzag_range(0, 1), [0]) + self.assertEqual(ESeriesUtil.zigzag_range(0, 2), [0, 1]) + self.assertEqual(ESeriesUtil.zigzag_range(0, 3), [1, 0, 2]) + self.assertEqual(ESeriesUtil.zigzag_range(0, 4), [1, 0, 2, 3]) - def test_preferred_number(self) -> None: - # Test a few different powers - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(0.09, 0.11), ESeriesUtil.SERIES[24], 0.01), - 0.1) - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(0.9, 1.1), ESeriesUtil.SERIES[24], 0.01), - 1) - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(9, 11), ESeriesUtil.SERIES[24], 0.01), - 10) - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(900, 1100), ESeriesUtil.SERIES[24], 0.01), - 1000) + def test_preferred_number(self) -> None: + # Test a few different powers + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(0.09, 0.11), ESeriesUtil.SERIES[24], 0.01), 0.1) + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(0.9, 1.1), ESeriesUtil.SERIES[24], 0.01), 1) + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(9, 11), ESeriesUtil.SERIES[24], 0.01), 10) + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(900, 1100), ESeriesUtil.SERIES[24], 0.01), 1000) - # Test higher series - self.assertEqual( - ESeriesUtil.choose_preferred_number(Range.from_tolerance(965, 0.01), ESeriesUtil.SERIES[24], 0.01), - None) - self.assertEqual( - ESeriesUtil.choose_preferred_number(Range.from_tolerance(965, 0.01), ESeriesUtil.SERIES[192], 0.01), - 965) + # Test higher series + self.assertEqual( + ESeriesUtil.choose_preferred_number(Range.from_tolerance(965, 0.01), ESeriesUtil.SERIES[24], 0.01), None + ) + self.assertEqual( + ESeriesUtil.choose_preferred_number(Range.from_tolerance(965, 0.01), ESeriesUtil.SERIES[192], 0.01), 965 + ) - # Test a few different numbers - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(8100, 8300), ESeriesUtil.SERIES[24], 0.01), - 8200) - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(460, 480), ESeriesUtil.SERIES[24], 0.01), - 470) + # Test a few different numbers + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(8100, 8300), ESeriesUtil.SERIES[24], 0.01), 8200) + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(460, 480), ESeriesUtil.SERIES[24], 0.01), 470) - # Test preference for lower E-series first - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(101, 9999), ESeriesUtil.SERIES[24], 0.01), - 1000) - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(220, 820), ESeriesUtil.SERIES[24], 0.01), - 470) + # Test preference for lower E-series first + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(101, 9999), ESeriesUtil.SERIES[24], 0.01), 1000) + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(220, 820), ESeriesUtil.SERIES[24], 0.01), 470) - # Test dynamic range edge cases - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(999, 1500), ESeriesUtil.SERIES[24], 0.01), - 1200) - self.assertEqual(ESeriesUtil.choose_preferred_number(Range(680, 1001), ESeriesUtil.SERIES[24], 0.01), - 820) + # Test dynamic range edge cases + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(999, 1500), ESeriesUtil.SERIES[24], 0.01), 1200) + self.assertEqual(ESeriesUtil.choose_preferred_number(Range(680, 1001), ESeriesUtil.SERIES[24], 0.01), 820) class RatioTestCase(unittest.TestCase): - def test_ratio_product(self) -> None: - self.assertEqual(ESeriesRatioUtil._generate_e_series_product([1, 2, 3, 4], 0, 0), - [(1, 1), - (2, 1), (1, 2), (2, 2), - (3, 1), (1, 3), (3, 2), (2, 3), (3, 3), - (4, 1), (1, 4), (4, 2), (2, 4), (4, 3), (3, 4), (4, 4)]) + def test_ratio_product(self) -> None: + self.assertEqual( + ESeriesRatioUtil._generate_e_series_product([1, 2, 3, 4], 0, 0), + [ + (1, 1), + (2, 1), + (1, 2), + (2, 2), + (3, 1), + (1, 3), + (3, 2), + (2, 3), + (3, 3), + (4, 1), + (1, 4), + (4, 2), + (2, 4), + (4, 3), + (3, 4), + (4, 4), + ], + ) - def test_series_of(self) -> None: - self.assertEqual(ESeriesUtil.series_of(1.0), 3) - self.assertEqual(ESeriesUtil.series_of(2.2), 3) - self.assertEqual(ESeriesUtil.series_of(6.8), 6) - self.assertEqual(ESeriesUtil.series_of(6800), 6) - self.assertEqual(ESeriesUtil.series_of(0.91), 24) - self.assertEqual(ESeriesUtil.series_of(0.01), 3) - self.assertEqual(ESeriesUtil.series_of(9.88), 192) - self.assertEqual(ESeriesUtil.series_of(0.42), None) - self.assertEqual(ESeriesUtil.series_of(0.42, default=1000), 1000) + def test_series_of(self) -> None: + self.assertEqual(ESeriesUtil.series_of(1.0), 3) + self.assertEqual(ESeriesUtil.series_of(2.2), 3) + self.assertEqual(ESeriesUtil.series_of(6.8), 6) + self.assertEqual(ESeriesUtil.series_of(6800), 6) + self.assertEqual(ESeriesUtil.series_of(0.91), 24) + self.assertEqual(ESeriesUtil.series_of(0.01), 3) + self.assertEqual(ESeriesUtil.series_of(9.88), 192) + self.assertEqual(ESeriesUtil.series_of(0.42), None) + self.assertEqual(ESeriesUtil.series_of(0.42, default=1000), 1000) diff --git a/edg/abstract_parts/test_ideal_circuit.py b/edg/abstract_parts/test_ideal_circuit.py index 4b8da1cb3..3683d8b5e 100644 --- a/edg/abstract_parts/test_ideal_circuit.py +++ b/edg/abstract_parts/test_ideal_circuit.py @@ -8,34 +8,34 @@ class IdealCircuitTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.gnd = self.Block(DummyGround()) - self.pwr = self.Block(DummyVoltageSource(5*Volt(tol=0))) - with self.implicit_connect( - ImplicitConnect(self.gnd.gnd, [Common]), - ) as imp: - self.reg = imp.Block(LinearRegulator(2*Volt(tol=0))) - self.connect(self.reg.pwr_in, self.pwr.pwr) - self.reg_draw = self.Block(DummyVoltageSink(current_draw=1*Amp(tol=0))) - self.connect(self.reg_draw.pwr, self.reg.pwr_out) - - self.boost = imp.Block(BoostConverter(4*Volt(tol=0))) - self.connect(self.boost.pwr_in, self.reg.pwr_out) - self.boost_draw = self.Block(DummyVoltageSink(current_draw=1*Amp(tol=0))) - self.connect(self.boost_draw.pwr, self.boost.pwr_out) # draws 2A from reg - - self.mcu = imp.Block(IoController()) - self.connect(self.mcu.pwr, self.reg.pwr_out) - self.mcu_io = self.Block(DummyDigitalSink()) - self.connect(self.mcu_io.io, self.mcu.gpio.request('test')) - - self.require(self.pwr.current_drawn == 3*Amp(tol=0)) - self.require(self.reg_draw.voltage == 2*Volt(tol=0)) + def __init__(self) -> None: + super().__init__() + self.gnd = self.Block(DummyGround()) + self.pwr = self.Block(DummyVoltageSource(5 * Volt(tol=0))) + with self.implicit_connect( + ImplicitConnect(self.gnd.gnd, [Common]), + ) as imp: + self.reg = imp.Block(LinearRegulator(2 * Volt(tol=0))) + self.connect(self.reg.pwr_in, self.pwr.pwr) + self.reg_draw = self.Block(DummyVoltageSink(current_draw=1 * Amp(tol=0))) + self.connect(self.reg_draw.pwr, self.reg.pwr_out) + + self.boost = imp.Block(BoostConverter(4 * Volt(tol=0))) + self.connect(self.boost.pwr_in, self.reg.pwr_out) + self.boost_draw = self.Block(DummyVoltageSink(current_draw=1 * Amp(tol=0))) + self.connect(self.boost_draw.pwr, self.boost.pwr_out) # draws 2A from reg + + self.mcu = imp.Block(IoController()) + self.connect(self.mcu.pwr, self.reg.pwr_out) + self.mcu_io = self.Block(DummyDigitalSink()) + self.connect(self.mcu_io.io, self.mcu.gpio.request("test")) + + self.require(self.pwr.current_drawn == 3 * Amp(tol=0)) + self.require(self.reg_draw.voltage == 2 * Volt(tol=0)) class IdealCircuitTest(unittest.TestCase): - def test_ideal_circuit(self) -> None: - ScalaCompiler.compile(IdealCircuitTestTop, refinements=Refinements( - class_values=[(IdealModel, ['allow_ideal'], True)] - )) + def test_ideal_circuit(self) -> None: + ScalaCompiler.compile( + IdealCircuitTestTop, refinements=Refinements(class_values=[(IdealModel, ["allow_ideal"], True)]) + ) diff --git a/edg/abstract_parts/test_kicad_import_netlist.py b/edg/abstract_parts/test_kicad_import_netlist.py index 8591a8756..fc7489c25 100644 --- a/edg/abstract_parts/test_kicad_import_netlist.py +++ b/edg/abstract_parts/test_kicad_import_netlist.py @@ -10,81 +10,135 @@ class PassiveDummy(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(Passive(), [InOut]) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(Passive(), [InOut]) class KiCadBlackboxTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(KiCadBlackboxBlock()) - (self.dummypwr, ), _ = self.chain(self.dut.pwr, self.Block(PassiveDummy())) - (self.dummygnd, ), _ = self.chain(self.dut.gnd, self.Block(PassiveDummy())) - (self.dummyout, ), _ = self.chain(self.dut.out, self.Block(PassiveDummy())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(KiCadBlackboxBlock()) + (self.dummypwr,), _ = self.chain(self.dut.pwr, self.Block(PassiveDummy())) + (self.dummygnd,), _ = self.chain(self.dut.gnd, self.Block(PassiveDummy())) + (self.dummyout,), _ = self.chain(self.dut.out, self.Block(PassiveDummy())) class DummyResistor(Resistor, FootprintBlock): - def __init__(self) -> None: - super().__init__(Range.all()) - self.footprint('R', 'Resistor_SMD:R_0603_1608Metric', - {'1': self.a, - '2': self.b, - }) + def __init__(self) -> None: + super().__init__(Range.all()) + self.footprint( + "R", + "Resistor_SMD:R_0603_1608Metric", + { + "1": self.a, + "2": self.b, + }, + ) class KiCadImportBlackboxTestCase(unittest.TestCase): - def test_netlist(self) -> None: - net = NetlistTestCase.generate_net(KiCadBlackboxTop, refinements=Refinements( - class_refinements=[ - (Resistor, DummyResistor), - ] - )) - # note, dut pruned out from paths since it's the only block in the top-level - self.assertIn(Net('dut.pwr', [ - NetPin(['dut', 'U1'], '1') - ], [ - TransformUtil.Path.empty().append_block('dut').append_port('pwr'), - TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '1'), - TransformUtil.Path.empty().append_block('dummypwr').append_port('port'), - ]), net.nets) - self.assertIn(Net('dut.gnd', [ - NetPin(['dut', 'U1'], '3') - ], [ - TransformUtil.Path.empty().append_block('dut').append_port('gnd'), - TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '3'), - TransformUtil.Path.empty().append_block('dummygnd').append_port('port'), - ]), net.nets) - self.assertIn(Net('dut.node', [ - NetPin(['dut', 'U1'], '2'), - NetPin(['dut', 'res'], '1') - ], [ - TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '2'), - TransformUtil.Path.empty().append_block('dut', 'res').append_port('a'), - ]), net.nets) - self.assertIn(Net('dut.out', [ - NetPin(['dut', 'res'], '2') - ], [ - TransformUtil.Path.empty().append_block('dut').append_port('out'), - TransformUtil.Path.empty().append_block('dut', 'res').append_port('b'), - TransformUtil.Path.empty().append_block('dummyout').append_port('port'), - ]), net.nets) - self.assertIn(NetBlock('Package_TO_SOT_SMD:SOT-23', 'U1', - # expected value is wonky because netlisting combines part and value - 'Sensor_Temperature:MCP9700AT-ETT', 'MCP9700AT-ETT', - ['dut', 'U1'], ['dut', 'U1'], - ['edg.electronics_model.test_kicad_import_blackbox.KiCadBlackboxBlock', - 'edg.electronics_model.KiCadSchematicBlock.KiCadBlackbox']), - net.blocks) - self.assertIn(NetBlock('Symbol:Symbol_ESD-Logo_CopperTop', 'SYM1', - # expected value is wonky because netlisting combines part and value - 'Graphic:SYM_ESD_Small', 'SYM_ESD_Small', - ['dut', 'SYM1'], ['dut', 'SYM1'], - ['edg.electronics_model.test_kicad_import_blackbox.KiCadBlackboxBlock', - 'edg.electronics_model.KiCadSchematicBlock.KiCadBlackbox']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '', - ['dut', 'res'], ['dut', 'res'], - ['edg.electronics_model.test_kicad_import_blackbox.KiCadBlackboxBlock', - 'edg.abstract_parts.test_kicad_import_netlist.DummyResistor']), - net.blocks) + def test_netlist(self) -> None: + net = NetlistTestCase.generate_net( + KiCadBlackboxTop, + refinements=Refinements( + class_refinements=[ + (Resistor, DummyResistor), + ] + ), + ) + # note, dut pruned out from paths since it's the only block in the top-level + self.assertIn( + Net( + "dut.pwr", + [NetPin(["dut", "U1"], "1")], + [ + TransformUtil.Path.empty().append_block("dut").append_port("pwr"), + TransformUtil.Path.empty().append_block("dut", "U1").append_port("ports", "1"), + TransformUtil.Path.empty().append_block("dummypwr").append_port("port"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "dut.gnd", + [NetPin(["dut", "U1"], "3")], + [ + TransformUtil.Path.empty().append_block("dut").append_port("gnd"), + TransformUtil.Path.empty().append_block("dut", "U1").append_port("ports", "3"), + TransformUtil.Path.empty().append_block("dummygnd").append_port("port"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "dut.node", + [NetPin(["dut", "U1"], "2"), NetPin(["dut", "res"], "1")], + [ + TransformUtil.Path.empty().append_block("dut", "U1").append_port("ports", "2"), + TransformUtil.Path.empty().append_block("dut", "res").append_port("a"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "dut.out", + [NetPin(["dut", "res"], "2")], + [ + TransformUtil.Path.empty().append_block("dut").append_port("out"), + TransformUtil.Path.empty().append_block("dut", "res").append_port("b"), + TransformUtil.Path.empty().append_block("dummyout").append_port("port"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Package_TO_SOT_SMD:SOT-23", + "U1", + # expected value is wonky because netlisting combines part and value + "Sensor_Temperature:MCP9700AT-ETT", + "MCP9700AT-ETT", + ["dut", "U1"], + ["dut", "U1"], + [ + "edg.electronics_model.test_kicad_import_blackbox.KiCadBlackboxBlock", + "edg.electronics_model.KiCadSchematicBlock.KiCadBlackbox", + ], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Symbol:Symbol_ESD-Logo_CopperTop", + "SYM1", + # expected value is wonky because netlisting combines part and value + "Graphic:SYM_ESD_Small", + "SYM_ESD_Small", + ["dut", "SYM1"], + ["dut", "SYM1"], + [ + "edg.electronics_model.test_kicad_import_blackbox.KiCadBlackboxBlock", + "edg.electronics_model.KiCadSchematicBlock.KiCadBlackbox", + ], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "", + ["dut", "res"], + ["dut", "res"], + [ + "edg.electronics_model.test_kicad_import_blackbox.KiCadBlackboxBlock", + "edg.abstract_parts.test_kicad_import_netlist.DummyResistor", + ], + ), + net.blocks, + ) diff --git a/edg/abstract_parts/test_kicad_part_parsing.py b/edg/abstract_parts/test_kicad_part_parsing.py index 0355c747f..a12659ba8 100644 --- a/edg/abstract_parts/test_kicad_part_parsing.py +++ b/edg/abstract_parts/test_kicad_part_parsing.py @@ -4,24 +4,27 @@ class KicadPartParsingTest(unittest.TestCase): - def test_resistor(self) -> None: - self.assertEqual(Resistor.parse_resistor("51"), Range.from_tolerance(51, 0.05)) - self.assertEqual(Resistor.parse_resistor("22kR"), Range.from_tolerance(22000, 0.05)) - self.assertEqual(Resistor.parse_resistor("22k"), Range.from_tolerance(22000, 0.05)) - self.assertEqual(Resistor.parse_resistor("22 k"), Range.from_tolerance(22000, 0.05)) - self.assertEqual(Resistor.parse_resistor("22k0"), Range.from_tolerance(22000, 0.05)) - self.assertEqual(Resistor.parse_resistor("2k2"), Range.from_tolerance(2200, 0.05)) + def test_resistor(self) -> None: + self.assertEqual(Resistor.parse_resistor("51"), Range.from_tolerance(51, 0.05)) + self.assertEqual(Resistor.parse_resistor("22kR"), Range.from_tolerance(22000, 0.05)) + self.assertEqual(Resistor.parse_resistor("22k"), Range.from_tolerance(22000, 0.05)) + self.assertEqual(Resistor.parse_resistor("22 k"), Range.from_tolerance(22000, 0.05)) + self.assertEqual(Resistor.parse_resistor("22k0"), Range.from_tolerance(22000, 0.05)) + self.assertEqual(Resistor.parse_resistor("2k2"), Range.from_tolerance(2200, 0.05)) - self.assertEqual(Resistor.parse_resistor("22k 10%"), Range.from_tolerance(22000, 0.1)) - self.assertEqual(Resistor.parse_resistor("22k ±10%"), Range.from_tolerance(22000, 0.1)) - self.assertEqual(Resistor.parse_resistor("22k 5%"), Range.from_tolerance(22000, 0.05)) - self.assertEqual(Resistor.parse_resistor("22k0 5%"), Range.from_tolerance(22000, 0.05)) - self.assertEqual(Resistor.parse_resistor("2k2 5%"), Range.from_tolerance(2200, 0.05)) + self.assertEqual(Resistor.parse_resistor("22k 10%"), Range.from_tolerance(22000, 0.1)) + self.assertEqual(Resistor.parse_resistor("22k ±10%"), Range.from_tolerance(22000, 0.1)) + self.assertEqual(Resistor.parse_resistor("22k 5%"), Range.from_tolerance(22000, 0.05)) + self.assertEqual(Resistor.parse_resistor("22k0 5%"), Range.from_tolerance(22000, 0.05)) + self.assertEqual(Resistor.parse_resistor("2k2 5%"), Range.from_tolerance(2200, 0.05)) - def test_capacitor(self) -> None: - self.assertEqual(Capacitor.parse_capacitor("0.1uF 6.3V"), (Range.from_tolerance(0.1e-6, 0.20), - Range.zero_to_upper(6.3))) - self.assertEqual(Capacitor.parse_capacitor("4.7u 6.3V"), (Range.from_tolerance(4.7e-6, 0.20), - Range.zero_to_upper(6.3))) - self.assertEqual(Capacitor.parse_capacitor("0.1uF 10% 12V"), (Range.from_tolerance(0.1e-6, 0.10), - Range.zero_to_upper(12))) + def test_capacitor(self) -> None: + self.assertEqual( + Capacitor.parse_capacitor("0.1uF 6.3V"), (Range.from_tolerance(0.1e-6, 0.20), Range.zero_to_upper(6.3)) + ) + self.assertEqual( + Capacitor.parse_capacitor("4.7u 6.3V"), (Range.from_tolerance(4.7e-6, 0.20), Range.zero_to_upper(6.3)) + ) + self.assertEqual( + Capacitor.parse_capacitor("0.1uF 10% 12V"), (Range.from_tolerance(0.1e-6, 0.10), Range.zero_to_upper(12)) + ) diff --git a/edg/abstract_parts/test_opamp.py b/edg/abstract_parts/test_opamp.py index 4128dc4c0..1143de5bc 100644 --- a/edg/abstract_parts/test_opamp.py +++ b/edg/abstract_parts/test_opamp.py @@ -9,50 +9,51 @@ class AnalogSourceDummy(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(AnalogSource(), [InOut]) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(AnalogSource(), [InOut]) class TestOpamp(Opamp): - @override - def contents(self) -> None: - super().contents() - self.pwr.init_from(VoltageSink()) - self.gnd.init_from(Ground()) - self.inp.init_from(AnalogSink()) - self.inn.init_from(AnalogSink()) - self.out.init_from(AnalogSource()) + @override + def contents(self) -> None: + super().contents() + self.pwr.init_from(VoltageSink()) + self.gnd.init_from(Ground()) + self.inp.init_from(AnalogSink()) + self.inn.init_from(AnalogSink()) + self.out.init_from(AnalogSource()) class TestResistor(Resistor): - @override - def contents(self) -> None: - super().contents() - self.assign(self.actual_resistance, self.resistance) + @override + def contents(self) -> None: + super().contents() + self.assign(self.actual_resistance, self.resistance) class AmplifierTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(Amplifier( - amplification=Range.from_tolerance(2, 0.05) - )) - (self.dummyin, ), _ = self.chain(self.dut.input, self.Block(AnalogSourceDummy())) - (self.dummyref, ), _ = self.chain(self.dut.reference, self.Block(AnalogSourceDummy())) - (self.dummyout, ), _ = self.chain(self.dut.output, self.Block(DummyAnalogSink())) - (self.dummypwr, ), _ = self.chain(self.dut.pwr, self.Block(DummyVoltageSource())) - (self.dummygnd, ), _ = self.chain(self.dut.gnd, self.Block(DummyGround())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(Amplifier(amplification=Range.from_tolerance(2, 0.05))) + (self.dummyin,), _ = self.chain(self.dut.input, self.Block(AnalogSourceDummy())) + (self.dummyref,), _ = self.chain(self.dut.reference, self.Block(AnalogSourceDummy())) + (self.dummyout,), _ = self.chain(self.dut.output, self.Block(DummyAnalogSink())) + (self.dummypwr,), _ = self.chain(self.dut.pwr, self.Block(DummyVoltageSource())) + (self.dummygnd,), _ = self.chain(self.dut.gnd, self.Block(DummyGround())) class OpampCircuitTest(unittest.TestCase): - def test_opamp_amplifier(self) -> None: - compiled = ScalaCompiler.compile(AmplifierTestTop, refinements=Refinements( - class_refinements=[ - (Opamp, TestOpamp), - (Resistor, TestResistor), - ] - )) - - self.assertEqual(compiled.get_value(['dut', 'r1', 'resistance']), Range.from_tolerance(100e3, 0.01)) - self.assertEqual(compiled.get_value(['dut', 'r2', 'resistance']), Range.from_tolerance(100e3, 0.01)) + def test_opamp_amplifier(self) -> None: + compiled = ScalaCompiler.compile( + AmplifierTestTop, + refinements=Refinements( + class_refinements=[ + (Opamp, TestOpamp), + (Resistor, TestResistor), + ] + ), + ) + + self.assertEqual(compiled.get_value(["dut", "r1", "resistance"]), Range.from_tolerance(100e3, 0.01)) + self.assertEqual(compiled.get_value(["dut", "r2", "resistance"]), Range.from_tolerance(100e3, 0.01)) diff --git a/edg/abstract_parts/test_opamp_calculations.py b/edg/abstract_parts/test_opamp_calculations.py index 91305bb3a..96fbea9d8 100644 --- a/edg/abstract_parts/test_opamp_calculations.py +++ b/edg/abstract_parts/test_opamp_calculations.py @@ -5,12 +5,16 @@ class OpampCalculationsTest(unittest.TestCase): - def test_summing_amplifier(self) -> None: - self.assertEqual(SummingAmplifier.calculate_ratio( - [Range.exact(10e3), Range.exact(10e3)]), [Range.exact(0.5), Range.exact(0.5)]) - self.assertEqual(SummingAmplifier.calculate_ratio( - [Range.exact(10e3), Range.exact(10e3), Range.exact(20e3)]), - [Range.exact(0.4), Range.exact(0.4), Range.exact(0.2)]) - self.assertEqual(SummingAmplifier.calculate_ratio( - [Range.from_tolerance(10e3, 0.01), Range.from_tolerance(10e3, 0.01)]), - [Range(0.495, 0.505), Range(0.495, 0.505)]) + def test_summing_amplifier(self) -> None: + self.assertEqual( + SummingAmplifier.calculate_ratio([Range.exact(10e3), Range.exact(10e3)]), + [Range.exact(0.5), Range.exact(0.5)], + ) + self.assertEqual( + SummingAmplifier.calculate_ratio([Range.exact(10e3), Range.exact(10e3), Range.exact(20e3)]), + [Range.exact(0.4), Range.exact(0.4), Range.exact(0.2)], + ) + self.assertEqual( + SummingAmplifier.calculate_ratio([Range.from_tolerance(10e3, 0.01), Range.from_tolerance(10e3, 0.01)]), + [Range(0.495, 0.505), Range(0.495, 0.505)], + ) diff --git a/edg/abstract_parts/test_parts_table.py b/edg/abstract_parts/test_parts_table.py index 2c56bbf08..11e3e9ecd 100644 --- a/edg/abstract_parts/test_parts_table.py +++ b/edg/abstract_parts/test_parts_table.py @@ -7,120 +7,142 @@ class PartsTableTest(unittest.TestCase): - INT_COLUMN = PartsTableColumn(int) - - # TODO don't test using internal variables - @override - def setUp(self) -> None: - path = os.path.join(os.path.dirname(__file__), 'resources', 'test_table.csv') - with open(path, newline='') as csvfile: - reader = csv.DictReader(csvfile) - self.table = PartsTable.from_dict_rows([row for row in reader]) - - def test_product_table(self) -> None: - self.assertEqual(len(self.table.rows), 3) - self.assertEqual(self.table.rows[0].values, {'header1': '1', 'header2': 'foo', 'header3': '9'}) - self.assertEqual(self.table.rows[1].values, {'header1': '2', 'header2': 'bar', 'header3': '8'}) - self.assertEqual(self.table.rows[2].values, {'header1': '3', 'header2': 'ducks', 'header3': '7'}) - - def test_multiple(self) -> None: - path = os.path.join(os.path.dirname(__file__), 'resources', 'test_table.csv') - with open(path, newline='') as csvfile: - reader = csv.DictReader(csvfile) - rows = [row for row in reader] - table = PartsTable.from_dict_rows(rows, rows) - - self.assertEqual(len(table.rows), 6) - self.assertEqual(table.rows[0].values, {'header1': '1', 'header2': 'foo', 'header3': '9'}) - self.assertEqual(table.rows[1].values, {'header1': '2', 'header2': 'bar', 'header3': '8'}) - self.assertEqual(table.rows[2].values, {'header1': '3', 'header2': 'ducks', 'header3': '7'}) - self.assertEqual(table.rows[3].values, {'header1': '1', 'header2': 'foo', 'header3': '9'}) - self.assertEqual(table.rows[4].values, {'header1': '2', 'header2': 'bar', 'header3': '8'}) - self.assertEqual(table.rows[5].values, {'header1': '3', 'header2': 'ducks', 'header3': '7'}) - - def test_derived_filter(self) -> None: - table = self.table.filter(lambda row: row['header1'] != '2') - self.assertEqual(len(table.rows), 2) - self.assertEqual(table.rows[0].values, {'header1': '1', 'header2': 'foo', 'header3': '9'}) - self.assertEqual(table.rows[1].values, {'header1': '3', 'header2': 'ducks', 'header3': '7'}) - - def test_derived_column(self) -> None: - def parse_int(row: PartsTableRow) -> Dict[PartsTableColumn, Any]: - return { - self.INT_COLUMN: int(row['header1']) - } - table = self.table.map_new_columns(parse_int) - self.assertEqual(table.rows[0][self.INT_COLUMN], 1) - self.assertEqual(table.rows[1][self.INT_COLUMN], 2) - self.assertEqual(table.rows[2][self.INT_COLUMN], 3) - - def test_derived_column_filter(self) -> None: - def parse_int(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['header1'] == '2': - return None - else: - return {} - table = self.table.map_new_columns(parse_int) - self.assertEqual(len(table.rows), 2) - self.assertEqual(table.rows[0].values, {'header1': '1', 'header2': 'foo', 'header3': '9'}) - self.assertEqual(table.rows[1].values, {'header1': '3', 'header2': 'ducks', 'header3': '7'}) - - def test_sort(self) -> None: - table = self.table.sort_by(lambda row: row['header3']) - self.assertEqual(table.rows[0].values, {'header1': '3', 'header2': 'ducks', 'header3': '7'}) - self.assertEqual(table.rows[1].values, {'header1': '2', 'header2': 'bar', 'header3': '8'}) - self.assertEqual(table.rows[2].values, {'header1': '1', 'header2': 'foo', 'header3': '9'}) - - def test_map(self) -> None: - output = self.table.map(lambda row: float(row['header1'])) - self.assertEqual(output, [1, 2, 3]) - self.assertEqual(sum(output), 6) - - def test_first(self) -> None: - self.assertEqual(self.table.first().values, {'header1': '1', 'header2': 'foo', 'header3': '9'}) + INT_COLUMN = PartsTableColumn(int) + + # TODO don't test using internal variables + @override + def setUp(self) -> None: + path = os.path.join(os.path.dirname(__file__), "resources", "test_table.csv") + with open(path, newline="") as csvfile: + reader = csv.DictReader(csvfile) + self.table = PartsTable.from_dict_rows([row for row in reader]) + + def test_product_table(self) -> None: + self.assertEqual(len(self.table.rows), 3) + self.assertEqual(self.table.rows[0].values, {"header1": "1", "header2": "foo", "header3": "9"}) + self.assertEqual(self.table.rows[1].values, {"header1": "2", "header2": "bar", "header3": "8"}) + self.assertEqual(self.table.rows[2].values, {"header1": "3", "header2": "ducks", "header3": "7"}) + + def test_multiple(self) -> None: + path = os.path.join(os.path.dirname(__file__), "resources", "test_table.csv") + with open(path, newline="") as csvfile: + reader = csv.DictReader(csvfile) + rows = [row for row in reader] + table = PartsTable.from_dict_rows(rows, rows) + + self.assertEqual(len(table.rows), 6) + self.assertEqual(table.rows[0].values, {"header1": "1", "header2": "foo", "header3": "9"}) + self.assertEqual(table.rows[1].values, {"header1": "2", "header2": "bar", "header3": "8"}) + self.assertEqual(table.rows[2].values, {"header1": "3", "header2": "ducks", "header3": "7"}) + self.assertEqual(table.rows[3].values, {"header1": "1", "header2": "foo", "header3": "9"}) + self.assertEqual(table.rows[4].values, {"header1": "2", "header2": "bar", "header3": "8"}) + self.assertEqual(table.rows[5].values, {"header1": "3", "header2": "ducks", "header3": "7"}) + + def test_derived_filter(self) -> None: + table = self.table.filter(lambda row: row["header1"] != "2") + self.assertEqual(len(table.rows), 2) + self.assertEqual(table.rows[0].values, {"header1": "1", "header2": "foo", "header3": "9"}) + self.assertEqual(table.rows[1].values, {"header1": "3", "header2": "ducks", "header3": "7"}) + + def test_derived_column(self) -> None: + def parse_int(row: PartsTableRow) -> Dict[PartsTableColumn, Any]: + return {self.INT_COLUMN: int(row["header1"])} + + table = self.table.map_new_columns(parse_int) + self.assertEqual(table.rows[0][self.INT_COLUMN], 1) + self.assertEqual(table.rows[1][self.INT_COLUMN], 2) + self.assertEqual(table.rows[2][self.INT_COLUMN], 3) + + def test_derived_column_filter(self) -> None: + def parse_int(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["header1"] == "2": + return None + else: + return {} + + table = self.table.map_new_columns(parse_int) + self.assertEqual(len(table.rows), 2) + self.assertEqual(table.rows[0].values, {"header1": "1", "header2": "foo", "header3": "9"}) + self.assertEqual(table.rows[1].values, {"header1": "3", "header2": "ducks", "header3": "7"}) + + def test_sort(self) -> None: + table = self.table.sort_by(lambda row: row["header3"]) + self.assertEqual(table.rows[0].values, {"header1": "3", "header2": "ducks", "header3": "7"}) + self.assertEqual(table.rows[1].values, {"header1": "2", "header2": "bar", "header3": "8"}) + self.assertEqual(table.rows[2].values, {"header1": "1", "header2": "foo", "header3": "9"}) + + def test_map(self) -> None: + output = self.table.map(lambda row: float(row["header1"])) + self.assertEqual(output, [1, 2, 3]) + self.assertEqual(sum(output), 6) + + def test_first(self) -> None: + self.assertEqual(self.table.first().values, {"header1": "1", "header2": "foo", "header3": "9"}) class UserFnPartsTableTest(unittest.TestCase): - @staticmethod - @ExperimentalUserFnPartsTable.user_fn() - def user_fn_false() -> Callable[[], bool]: - def inner() -> bool: - return False - return inner - - @staticmethod - @ExperimentalUserFnPartsTable.user_fn([bool]) - def user_fn_bool_pass(meta_arg: bool) -> Callable[[], bool]: - def inner() -> bool: - return meta_arg - return inner - - @staticmethod - @ExperimentalUserFnPartsTable.user_fn([float]) - def user_fn_float_pass(meta_arg: float) -> Callable[[], float]: - def inner() -> float: - return meta_arg - return inner - - @staticmethod - def user_fn_unserialized() -> Callable[[], None]: - def inner() -> None: - return None - return inner - - def test_serialize_deserialize(self) -> None: - self.assertEqual(ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_false), 'user_fn_false') - self.assertEqual(ExperimentalUserFnPartsTable.deserialize_fn( - ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_false))(), False) - - self.assertEqual(ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, False), - 'user_fn_bool_pass;False') - self.assertEqual(ExperimentalUserFnPartsTable.deserialize_fn( - ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, False))(), False) - self.assertEqual(ExperimentalUserFnPartsTable.deserialize_fn( - ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, True))(), True) - - self.assertEqual(ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_float_pass, 0.42), - 'user_fn_float_pass;0.42') - self.assertEqual(ExperimentalUserFnPartsTable.deserialize_fn( - ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_float_pass, 0.42))(), 0.42) + @staticmethod + @ExperimentalUserFnPartsTable.user_fn() + def user_fn_false() -> Callable[[], bool]: + def inner() -> bool: + return False + + return inner + + @staticmethod + @ExperimentalUserFnPartsTable.user_fn([bool]) + def user_fn_bool_pass(meta_arg: bool) -> Callable[[], bool]: + def inner() -> bool: + return meta_arg + + return inner + + @staticmethod + @ExperimentalUserFnPartsTable.user_fn([float]) + def user_fn_float_pass(meta_arg: float) -> Callable[[], float]: + def inner() -> float: + return meta_arg + + return inner + + @staticmethod + def user_fn_unserialized() -> Callable[[], None]: + def inner() -> None: + return None + + return inner + + def test_serialize_deserialize(self) -> None: + self.assertEqual(ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_false), "user_fn_false") + self.assertEqual( + ExperimentalUserFnPartsTable.deserialize_fn( + ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_false) + )(), + False, + ) + + self.assertEqual( + ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, False), "user_fn_bool_pass;False" + ) + self.assertEqual( + ExperimentalUserFnPartsTable.deserialize_fn( + ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, False) + )(), + False, + ) + self.assertEqual( + ExperimentalUserFnPartsTable.deserialize_fn( + ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, True) + )(), + True, + ) + + self.assertEqual( + ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_float_pass, 0.42), "user_fn_float_pass;0.42" + ) + self.assertEqual( + ExperimentalUserFnPartsTable.deserialize_fn( + ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_float_pass, 0.42) + )(), + 0.42, + ) diff --git a/edg/abstract_parts/test_pinmappable.py b/edg/abstract_parts/test_pinmappable.py index 331fd8851..972d2c62a 100644 --- a/edg/abstract_parts/test_pinmappable.py +++ b/edg/abstract_parts/test_pinmappable.py @@ -1,212 +1,247 @@ import unittest from .PinMappable import UartPort, Volt, mAmp -from .PinMappable import PinMapUtil, PinResource, Passive, PeripheralFixedPin, UsbDevicePort, PeripheralAnyResource, \ - DigitalBidir, AnalogSink, AllocatedResource, AutomaticAllocationError, BadUserAssignError, PeripheralFixedResource +from .PinMappable import ( + PinMapUtil, + PinResource, + Passive, + PeripheralFixedPin, + UsbDevicePort, + PeripheralAnyResource, + DigitalBidir, + AnalogSink, + AllocatedResource, + AutomaticAllocationError, + BadUserAssignError, + PeripheralFixedResource, +) class PinMapUtilTest(unittest.TestCase): - def test_remap(self) -> None: - mapper = PinMapUtil([ - PinResource('PIO1', {'PIO1': Passive()}), - PinResource('PIO2', {'PIO2': Passive()}), # dropped - PeripheralFixedPin('Per1', UsbDevicePort(), {'dp': 'PIO4', 'dm': 'PIO6'}), - PeripheralAnyResource('Per2', UsbDevicePort()), - ]) - remapped = mapper.remap_pins({ - 'PIO1': '1', - 'PIO4': '4', - 'PIO6': '6', - }) - - assert isinstance(remapped.resources[0], PinResource) # typer doesn't understand asserttrue - assert isinstance(mapper.resources[0], PinResource) - self.assertEqual(remapped.resources[0].pin, '1') - self.assertTrue(remapped.resources[0].name_models is mapper.resources[0].name_models) # to avoid __eq__ on models - - assert isinstance(remapped.resources[1], PeripheralFixedPin) - assert isinstance(mapper.resources[2], PeripheralFixedPin) - self.assertEqual(remapped.resources[1].name, 'Per1') - self.assertTrue(remapped.resources[1].port_model is mapper.resources[2].port_model) - self.assertEqual(remapped.resources[1].inner_allowed_pins, {'dp': '4', 'dm': '6'}) - - self.assertTrue(remapped.resources[2] is mapper.resources[3]) # simple passthrough - - def test_assign_assigned(self) -> None: # fully user-specified - dio_model = DigitalBidir() - ain_model = AnalogSink() - allocated = PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('2', {'PIO2': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - PinResource('4', {'PIO4': dio_model, 'AIn4': ain_model}), - PinResource('5', {'AIn5': ain_model}), - ]).allocate([(DigitalBidir, ['DIO3', 'DIO2']), (AnalogSink, ['AIO4', 'AIO5'])], - ["DIO3=3", "DIO2=2", "AIO4=4", "AIO5=5"]) - self.assertIn(AllocatedResource(dio_model, 'DIO3', 'PIO3', '3'), allocated) - self.assertIn(AllocatedResource(dio_model, 'DIO2', 'PIO2', '2'), allocated) - self.assertIn(AllocatedResource(ain_model, 'AIO4', 'AIn4', '4'), allocated) - self.assertIn(AllocatedResource(ain_model, 'AIO5', 'AIn5', '5'), allocated) - - def test_assign_mixed(self) -> None: # mix of user-specified and automatic assignments, assuming greedy algo - dio_model = DigitalBidir() - ain_model = AnalogSink() - allocated = PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('2', {'PIO2': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - PinResource('4', {'PIO4': dio_model, 'AIn4': ain_model}), - PinResource('5', {'AIn5': ain_model}), - ]).allocate([(DigitalBidir, ['DIO3', 'DIO1']), (AnalogSink, ['AIO5', 'AIO4'])], - ["DIO3=3", "AIO4=4"]) - self.assertIn(AllocatedResource(dio_model, 'DIO3', 'PIO3', '3'), allocated) - self.assertIn(AllocatedResource(dio_model, 'DIO1', 'PIO1', '1'), allocated) - self.assertIn(AllocatedResource(ain_model, 'AIO4', 'AIn4', '4'), allocated) - self.assertIn(AllocatedResource(ain_model, 'AIO5', 'AIn5', '5'), allocated) - - def test_assign_bad(self) -> None: # bad user-specified assignments - dio_model = DigitalBidir() - ain_model = AnalogSink() - with self.assertRaises(BadUserAssignError): - PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - ]).allocate([(AnalogSink, ['AIO'])], - ["AIO=1"]) - - def test_assign_duplicated(self) -> None: # duplicated (over-assigned resources) user-specified assignments - dio_model = DigitalBidir() - ain_model = AnalogSink() - with self.assertRaises(BadUserAssignError): - PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - ]).allocate([(AnalogSink, ['AIO1', 'AIO2'])], - ["AIO1=3", "AIO2=3"]) - - def test_assign_overflow(self) -> None: # more requested ports than available resources - dio_model = DigitalBidir() - ain_model = AnalogSink() - with self.assertRaises(AutomaticAllocationError): - PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - ]).allocate([(AnalogSink, ['AIO3', 'AIO4'])]) - - def test_assign_bundle_fixed(self) -> None: - usb_model = UsbDevicePort() - allocated = PinMapUtil([ - PeripheralFixedPin('USB0', usb_model, {'dm': '2', 'dp': '3'}), - ]).allocate([(UsbDevicePort, ['usb'])], - ["usb.dm=2", "usb.dp=3"]) - self.assertIn(AllocatedResource(usb_model, 'usb', 'USB0', {'dm': ('2', None), 'dp': ('3', None)}), allocated) - - def test_assign_bundle_fixed_auto(self) -> None: - usb_model = UsbDevicePort() - allocated = PinMapUtil([ - PeripheralFixedPin('USB0', usb_model, {'dm': '2', 'dp': '3'}), - ]).allocate([(UsbDevicePort, ['usb'])]) - self.assertIn(AllocatedResource(usb_model, 'usb', 'USB0', {'dm': ('2', None), 'dp': ('3', None)}), allocated) - - def test_assign_bundle_fixed_badspec(self) -> None: - usb_model = UsbDevicePort() - with self.assertRaises(BadUserAssignError): - PinMapUtil([ - PeripheralFixedPin('USB0', usb_model, {'dm': '2', 'dp': '3'}), - ]).allocate([(UsbDevicePort, ['usb'])], - ["usb.dm=2", "usb.dp=5"]) - with self.assertRaises(BadUserAssignError): - PinMapUtil([ - PeripheralFixedPin('USB0', usb_model, {'dm': '2', 'dp': '3'}), - ]).allocate([(UsbDevicePort, ['usb'])], - ["usb.quack=1"]) - - def test_assign_bundle_delegating(self) -> None: - dio_model = DigitalBidir() - ain_model = AnalogSink() - allocated = PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('2', {'PIO2': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - PinResource('5', {'AIn5': ain_model}), # not assignable - PeripheralAnyResource('UART0', UartPort(DigitalBidir.empty())), - ]).allocate([(UartPort, ['uart'])], - ["uart.tx=1", "uart.rx=3"]) - self.assertEqual(allocated[0].name, 'uart') - self.assertEqual(allocated[0].resource_name, 'UART0') - self.assertEqual(allocated[0].pin, {'tx': ('1', 'PIO1'), 'rx': ('3', 'PIO3')}) - - def test_assign_bundle_delegating_auto(self) -> None: - dio_model = DigitalBidir() - ain_model = AnalogSink() - allocated = PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('2', {'PIO2': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - PinResource('5', {'AIn5': ain_model}), # not assignable - PeripheralAnyResource('UART0', UartPort(DigitalBidir.empty())), - ]).allocate([(UartPort, ['uart'])]) - self.assertEqual(allocated[0].name, 'uart') - self.assertEqual(allocated[0].resource_name, 'UART0') - self.assertEqual(allocated[0].pin, {'tx': ('1', 'PIO1'), 'rx': ('2', 'PIO2')}) - - def test_assign_bundle_delegating_badspec(self) -> None: - dio_model = DigitalBidir() - ain_model = AnalogSink() - with self.assertRaises(BadUserAssignError): - PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('2', {'PIO2': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - PinResource('5', {'AIn5': ain_model}), # not assignable - PeripheralAnyResource('UART0', UartPort(DigitalBidir.empty())), - ]).allocate([(UartPort, ['uart'])], - ["uart.tx=1", "uart.rx=5"]) - with self.assertRaises(BadUserAssignError): - PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PinResource('2', {'PIO2': dio_model}), - PinResource('3', {'PIO3': dio_model, 'AIn3': ain_model}), - PinResource('5', {'AIn5': ain_model}), # not assignable - PeripheralAnyResource('UART0', UartPort(DigitalBidir.empty())), - ]).allocate([(UartPort, ['uart'])], - ["uart.quack=1"]) - - def test_assign_bundle_delegating_fixed(self) -> None: - dio_model = DigitalBidir() - dio_model_tx = DigitalBidir( - voltage_out=3.3 * Volt(tol=0.01), - ) - dio_model_rx = DigitalBidir( - current_draw=1 * mAmp(tol=0.01) - ) - ain_model = AnalogSink() - allocated = PinMapUtil([ - PinResource('1', {'PIO1': dio_model_rx}), - PinResource('2', {'PIO2': dio_model}), - PinResource('3', {'PIO3': dio_model_tx, 'AIn3': ain_model}), - PinResource('5', {'AIn5': ain_model}), # not assignable - PeripheralFixedResource('UART0', UartPort(DigitalBidir.empty()), { - 'tx': ['PIO3'], 'rx': ['PIO1'] - }), - ]).allocate([(UartPort, ['uart'])]) - self.assertEqual(allocated[0].name, 'uart') - self.assertEqual(allocated[0].resource_name, 'UART0') - self.assertEqual(allocated[0].pin, {'tx': ('3', 'PIO3'), 'rx': ('1', 'PIO1')}) - - assert isinstance(allocated[0].port_model, UartPort) - self.assertTrue(allocated[0].port_model.tx.voltage_out.initializer is not None) - self.assertTrue(allocated[0].port_model.tx.voltage_out.initializer is dio_model_tx.voltage_out.initializer) - self.assertTrue(allocated[0].port_model.rx.current_draw.initializer is not None) - self.assertTrue(allocated[0].port_model.rx.current_draw.initializer is dio_model_rx.current_draw.initializer) - - def test_assign_bundle_delegating_notconnected(self) -> None: - dio_model = DigitalBidir() - allocated = PinMapUtil([ - PinResource('1', {'PIO1': dio_model}), - PeripheralAnyResource('UART0', UartPort(DigitalBidir.empty())), - ]).allocate([(UartPort, ['uart'])], - ['uart.tx=NC']) - self.assertEqual(allocated[0].name, 'uart') - self.assertEqual(allocated[0].resource_name, 'UART0') - self.assertEqual(allocated[0].pin, {'rx': ('1', 'PIO1')}) + def test_remap(self) -> None: + mapper = PinMapUtil( + [ + PinResource("PIO1", {"PIO1": Passive()}), + PinResource("PIO2", {"PIO2": Passive()}), # dropped + PeripheralFixedPin("Per1", UsbDevicePort(), {"dp": "PIO4", "dm": "PIO6"}), + PeripheralAnyResource("Per2", UsbDevicePort()), + ] + ) + remapped = mapper.remap_pins( + { + "PIO1": "1", + "PIO4": "4", + "PIO6": "6", + } + ) + + assert isinstance(remapped.resources[0], PinResource) # typer doesn't understand asserttrue + assert isinstance(mapper.resources[0], PinResource) + self.assertEqual(remapped.resources[0].pin, "1") + self.assertTrue( + remapped.resources[0].name_models is mapper.resources[0].name_models + ) # to avoid __eq__ on models + + assert isinstance(remapped.resources[1], PeripheralFixedPin) + assert isinstance(mapper.resources[2], PeripheralFixedPin) + self.assertEqual(remapped.resources[1].name, "Per1") + self.assertTrue(remapped.resources[1].port_model is mapper.resources[2].port_model) + self.assertEqual(remapped.resources[1].inner_allowed_pins, {"dp": "4", "dm": "6"}) + + self.assertTrue(remapped.resources[2] is mapper.resources[3]) # simple passthrough + + def test_assign_assigned(self) -> None: # fully user-specified + dio_model = DigitalBidir() + ain_model = AnalogSink() + allocated = PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("2", {"PIO2": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + PinResource("4", {"PIO4": dio_model, "AIn4": ain_model}), + PinResource("5", {"AIn5": ain_model}), + ] + ).allocate( + [(DigitalBidir, ["DIO3", "DIO2"]), (AnalogSink, ["AIO4", "AIO5"])], ["DIO3=3", "DIO2=2", "AIO4=4", "AIO5=5"] + ) + self.assertIn(AllocatedResource(dio_model, "DIO3", "PIO3", "3"), allocated) + self.assertIn(AllocatedResource(dio_model, "DIO2", "PIO2", "2"), allocated) + self.assertIn(AllocatedResource(ain_model, "AIO4", "AIn4", "4"), allocated) + self.assertIn(AllocatedResource(ain_model, "AIO5", "AIn5", "5"), allocated) + + def test_assign_mixed(self) -> None: # mix of user-specified and automatic assignments, assuming greedy algo + dio_model = DigitalBidir() + ain_model = AnalogSink() + allocated = PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("2", {"PIO2": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + PinResource("4", {"PIO4": dio_model, "AIn4": ain_model}), + PinResource("5", {"AIn5": ain_model}), + ] + ).allocate([(DigitalBidir, ["DIO3", "DIO1"]), (AnalogSink, ["AIO5", "AIO4"])], ["DIO3=3", "AIO4=4"]) + self.assertIn(AllocatedResource(dio_model, "DIO3", "PIO3", "3"), allocated) + self.assertIn(AllocatedResource(dio_model, "DIO1", "PIO1", "1"), allocated) + self.assertIn(AllocatedResource(ain_model, "AIO4", "AIn4", "4"), allocated) + self.assertIn(AllocatedResource(ain_model, "AIO5", "AIn5", "5"), allocated) + + def test_assign_bad(self) -> None: # bad user-specified assignments + dio_model = DigitalBidir() + ain_model = AnalogSink() + with self.assertRaises(BadUserAssignError): + PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + ] + ).allocate([(AnalogSink, ["AIO"])], ["AIO=1"]) + + def test_assign_duplicated(self) -> None: # duplicated (over-assigned resources) user-specified assignments + dio_model = DigitalBidir() + ain_model = AnalogSink() + with self.assertRaises(BadUserAssignError): + PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + ] + ).allocate([(AnalogSink, ["AIO1", "AIO2"])], ["AIO1=3", "AIO2=3"]) + + def test_assign_overflow(self) -> None: # more requested ports than available resources + dio_model = DigitalBidir() + ain_model = AnalogSink() + with self.assertRaises(AutomaticAllocationError): + PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + ] + ).allocate([(AnalogSink, ["AIO3", "AIO4"])]) + + def test_assign_bundle_fixed(self) -> None: + usb_model = UsbDevicePort() + allocated = PinMapUtil( + [ + PeripheralFixedPin("USB0", usb_model, {"dm": "2", "dp": "3"}), + ] + ).allocate([(UsbDevicePort, ["usb"])], ["usb.dm=2", "usb.dp=3"]) + self.assertIn(AllocatedResource(usb_model, "usb", "USB0", {"dm": ("2", None), "dp": ("3", None)}), allocated) + + def test_assign_bundle_fixed_auto(self) -> None: + usb_model = UsbDevicePort() + allocated = PinMapUtil( + [ + PeripheralFixedPin("USB0", usb_model, {"dm": "2", "dp": "3"}), + ] + ).allocate([(UsbDevicePort, ["usb"])]) + self.assertIn(AllocatedResource(usb_model, "usb", "USB0", {"dm": ("2", None), "dp": ("3", None)}), allocated) + + def test_assign_bundle_fixed_badspec(self) -> None: + usb_model = UsbDevicePort() + with self.assertRaises(BadUserAssignError): + PinMapUtil( + [ + PeripheralFixedPin("USB0", usb_model, {"dm": "2", "dp": "3"}), + ] + ).allocate([(UsbDevicePort, ["usb"])], ["usb.dm=2", "usb.dp=5"]) + with self.assertRaises(BadUserAssignError): + PinMapUtil( + [ + PeripheralFixedPin("USB0", usb_model, {"dm": "2", "dp": "3"}), + ] + ).allocate([(UsbDevicePort, ["usb"])], ["usb.quack=1"]) + + def test_assign_bundle_delegating(self) -> None: + dio_model = DigitalBidir() + ain_model = AnalogSink() + allocated = PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("2", {"PIO2": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + PinResource("5", {"AIn5": ain_model}), # not assignable + PeripheralAnyResource("UART0", UartPort(DigitalBidir.empty())), + ] + ).allocate([(UartPort, ["uart"])], ["uart.tx=1", "uart.rx=3"]) + self.assertEqual(allocated[0].name, "uart") + self.assertEqual(allocated[0].resource_name, "UART0") + self.assertEqual(allocated[0].pin, {"tx": ("1", "PIO1"), "rx": ("3", "PIO3")}) + + def test_assign_bundle_delegating_auto(self) -> None: + dio_model = DigitalBidir() + ain_model = AnalogSink() + allocated = PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("2", {"PIO2": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + PinResource("5", {"AIn5": ain_model}), # not assignable + PeripheralAnyResource("UART0", UartPort(DigitalBidir.empty())), + ] + ).allocate([(UartPort, ["uart"])]) + self.assertEqual(allocated[0].name, "uart") + self.assertEqual(allocated[0].resource_name, "UART0") + self.assertEqual(allocated[0].pin, {"tx": ("1", "PIO1"), "rx": ("2", "PIO2")}) + + def test_assign_bundle_delegating_badspec(self) -> None: + dio_model = DigitalBidir() + ain_model = AnalogSink() + with self.assertRaises(BadUserAssignError): + PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("2", {"PIO2": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + PinResource("5", {"AIn5": ain_model}), # not assignable + PeripheralAnyResource("UART0", UartPort(DigitalBidir.empty())), + ] + ).allocate([(UartPort, ["uart"])], ["uart.tx=1", "uart.rx=5"]) + with self.assertRaises(BadUserAssignError): + PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PinResource("2", {"PIO2": dio_model}), + PinResource("3", {"PIO3": dio_model, "AIn3": ain_model}), + PinResource("5", {"AIn5": ain_model}), # not assignable + PeripheralAnyResource("UART0", UartPort(DigitalBidir.empty())), + ] + ).allocate([(UartPort, ["uart"])], ["uart.quack=1"]) + + def test_assign_bundle_delegating_fixed(self) -> None: + dio_model = DigitalBidir() + dio_model_tx = DigitalBidir( + voltage_out=3.3 * Volt(tol=0.01), + ) + dio_model_rx = DigitalBidir(current_draw=1 * mAmp(tol=0.01)) + ain_model = AnalogSink() + allocated = PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model_rx}), + PinResource("2", {"PIO2": dio_model}), + PinResource("3", {"PIO3": dio_model_tx, "AIn3": ain_model}), + PinResource("5", {"AIn5": ain_model}), # not assignable + PeripheralFixedResource("UART0", UartPort(DigitalBidir.empty()), {"tx": ["PIO3"], "rx": ["PIO1"]}), + ] + ).allocate([(UartPort, ["uart"])]) + self.assertEqual(allocated[0].name, "uart") + self.assertEqual(allocated[0].resource_name, "UART0") + self.assertEqual(allocated[0].pin, {"tx": ("3", "PIO3"), "rx": ("1", "PIO1")}) + + assert isinstance(allocated[0].port_model, UartPort) + self.assertTrue(allocated[0].port_model.tx.voltage_out.initializer is not None) + self.assertTrue(allocated[0].port_model.tx.voltage_out.initializer is dio_model_tx.voltage_out.initializer) + self.assertTrue(allocated[0].port_model.rx.current_draw.initializer is not None) + self.assertTrue(allocated[0].port_model.rx.current_draw.initializer is dio_model_rx.current_draw.initializer) + + def test_assign_bundle_delegating_notconnected(self) -> None: + dio_model = DigitalBidir() + allocated = PinMapUtil( + [ + PinResource("1", {"PIO1": dio_model}), + PeripheralAnyResource("UART0", UartPort(DigitalBidir.empty())), + ] + ).allocate([(UartPort, ["uart"])], ["uart.tx=NC"]) + self.assertEqual(allocated[0].name, "uart") + self.assertEqual(allocated[0].resource_name, "UART0") + self.assertEqual(allocated[0].pin, {"rx": ("1", "PIO1")}) diff --git a/edg/abstract_parts/test_power_circuits.py b/edg/abstract_parts/test_power_circuits.py index 7bccbc55f..28a1f48ac 100644 --- a/edg/abstract_parts/test_power_circuits.py +++ b/edg/abstract_parts/test_power_circuits.py @@ -8,31 +8,34 @@ class RampLimiterTestTop(DesignTop): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(RampLimiter()) - - (self.dummyin, ), _ = self.chain(self.dut.pwr_in, self.Block(DummyVoltageSource(voltage_out=12*Volt(tol=0)))) - (self.dummyout, ), _ = self.chain(self.dut.pwr_out, self.Block(DummyVoltageSink(current_draw=1*Amp(tol=0)))) - (self.dummyctl, ), _ = self.chain(self.dut.control, self.Block(DummyDigitalSource(voltage_out=3.3*Volt(tol=0)))) - (self.dummygnd, ), _ = self.chain(self.dut.gnd, self.Block(DummyGround())) - - @override - def refinements(self) -> Refinements: - return Refinements( - class_refinements=[ - (Resistor, GenericChipResistor), - (Capacitor, GenericMlcc), - (Fet, CustomFet), - (SwitchFet, CustomFet), - ], instance_values=[ - (['dut', 'drv', 'footprint_spec'], 'Package_TO_SOT_SMD:SOT-23'), - (['dut', 'ctl_fet', 'footprint_spec'], 'Package_TO_SOT_SMD:SOT-23'), - (['dut', 'drv', 'actual_gate_drive'], Range(1.0, 12)), - ] - ) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(RampLimiter()) + + (self.dummyin,), _ = self.chain(self.dut.pwr_in, self.Block(DummyVoltageSource(voltage_out=12 * Volt(tol=0)))) + (self.dummyout,), _ = self.chain(self.dut.pwr_out, self.Block(DummyVoltageSink(current_draw=1 * Amp(tol=0)))) + (self.dummyctl,), _ = self.chain( + self.dut.control, self.Block(DummyDigitalSource(voltage_out=3.3 * Volt(tol=0))) + ) + (self.dummygnd,), _ = self.chain(self.dut.gnd, self.Block(DummyGround())) + + @override + def refinements(self) -> Refinements: + return Refinements( + class_refinements=[ + (Resistor, GenericChipResistor), + (Capacitor, GenericMlcc), + (Fet, CustomFet), + (SwitchFet, CustomFet), + ], + instance_values=[ + (["dut", "drv", "footprint_spec"], "Package_TO_SOT_SMD:SOT-23"), + (["dut", "ctl_fet", "footprint_spec"], "Package_TO_SOT_SMD:SOT-23"), + (["dut", "drv", "actual_gate_drive"], Range(1.0, 12)), + ], + ) class RampLimiterTest(unittest.TestCase): - def test_ramp_limiter(self) -> None: - ScalaCompiler.compile(RampLimiterTestTop) + def test_ramp_limiter(self) -> None: + ScalaCompiler.compile(RampLimiterTestTop) diff --git a/edg/abstract_parts/test_resistive_divider.py b/edg/abstract_parts/test_resistive_divider.py index 2a5e74674..740614c93 100644 --- a/edg/abstract_parts/test_resistive_divider.py +++ b/edg/abstract_parts/test_resistive_divider.py @@ -6,54 +6,46 @@ class ResistorDividerTest(unittest.TestCase): - def test_resistor_divider(self) -> None: - calculator = ESeriesRatioUtil(ESeriesUtil.SERIES[24], 0.01, DividerValues) - - self.assertEqual( - calculator.find(DividerValues(Range(0, 1), Range(0.1, 1))), - (1, 1)) - - self.assertEqual( - calculator.find(DividerValues(Range(0.48, 0.52), Range(0.1, 1))), # test a tighter range - (1, 1)) - - self.assertEqual( - calculator.find(DividerValues(Range(0.106, 0.111), Range(0.1, 1))), # test E12 - (8.2, 1)) - - self.assertEqual( - calculator.find(DividerValues(Range(0.208, 0.215), Range(1, 10))), # test E12 - (8.2, 2.2)) - - self.assertEqual( - calculator.find(DividerValues(Range(0.7241, 0.7321), Range(1, 10))), # test E12 - (5.6, 15)) - - self.assertEqual( - calculator.find(DividerValues(Range(0, 1), Range(10, 100))), # test impedance decade shift - (100, 100)) - - self.assertEqual( - calculator.find(DividerValues(Range(0.106, 0.111), Range(11, 99))), # test everything - (820, 100)) - - def test_impossible(self) -> None: - e1_calculator = ESeriesRatioUtil([1.0], 0.01, DividerValues) - - with self.assertRaises(ESeriesRatioUtil.NoMatchException) as error: - self.assertEqual( - e1_calculator.find(DividerValues(Range(0.10, 0.4), Range(0.1, 10))), # not possible with E1 series - None) - self.assertIn('with values (100.0, 10.0)', error.exception.args[0]) - - with self.assertRaises(ESeriesRatioUtil.NoMatchException): - self.assertEqual( - e1_calculator.find(DividerValues(Range(0.5, 0.5), Range(0.1, 10))), # tolerance too tight - None) - - with self.assertRaises(ESeriesRatioUtil.NoMatchException): - # this uses ratio = 0.1 - 0.9 for efficiency, otherwise the system searches keeps (fruitlessly) - # searching through more extreme ratios until it hits decade limits - self.assertEqual( - e1_calculator.find(DividerValues(Range(0.1, 0.9), Range(1, 4))), # can't meet the impedances - None) + def test_resistor_divider(self) -> None: + calculator = ESeriesRatioUtil(ESeriesUtil.SERIES[24], 0.01, DividerValues) + + self.assertEqual(calculator.find(DividerValues(Range(0, 1), Range(0.1, 1))), (1, 1)) + + self.assertEqual( + calculator.find(DividerValues(Range(0.48, 0.52), Range(0.1, 1))), (1, 1) # test a tighter range + ) + + self.assertEqual(calculator.find(DividerValues(Range(0.106, 0.111), Range(0.1, 1))), (8.2, 1)) # test E12 + + self.assertEqual(calculator.find(DividerValues(Range(0.208, 0.215), Range(1, 10))), (8.2, 2.2)) # test E12 + + self.assertEqual(calculator.find(DividerValues(Range(0.7241, 0.7321), Range(1, 10))), (5.6, 15)) # test E12 + + self.assertEqual( + calculator.find(DividerValues(Range(0, 1), Range(10, 100))), (100, 100) # test impedance decade shift + ) + + self.assertEqual( + calculator.find(DividerValues(Range(0.106, 0.111), Range(11, 99))), (820, 100) # test everything + ) + + def test_impossible(self) -> None: + e1_calculator = ESeriesRatioUtil([1.0], 0.01, DividerValues) + + with self.assertRaises(ESeriesRatioUtil.NoMatchException) as error: + self.assertEqual( + e1_calculator.find(DividerValues(Range(0.10, 0.4), Range(0.1, 10))), None # not possible with E1 series + ) + self.assertIn("with values (100.0, 10.0)", error.exception.args[0]) + + with self.assertRaises(ESeriesRatioUtil.NoMatchException): + self.assertEqual( + e1_calculator.find(DividerValues(Range(0.5, 0.5), Range(0.1, 10))), None # tolerance too tight + ) + + with self.assertRaises(ESeriesRatioUtil.NoMatchException): + # this uses ratio = 0.1 - 0.9 for efficiency, otherwise the system searches keeps (fruitlessly) + # searching through more extreme ratios until it hits decade limits + self.assertEqual( + e1_calculator.find(DividerValues(Range(0.1, 0.9), Range(1, 4))), None # can't meet the impedances + ) diff --git a/edg/abstract_parts/test_resistor_generic.py b/edg/abstract_parts/test_resistor_generic.py index ed857f5b8..09bd53ab8 100644 --- a/edg/abstract_parts/test_resistor_generic.py +++ b/edg/abstract_parts/test_resistor_generic.py @@ -4,64 +4,64 @@ class ResistorTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericChipResistor( - resistance=1 * kOhm(tol=0.1), - )) - (self.dummya, ), _ = self.chain(self.dut.a, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.b, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block( + GenericChipResistor( + resistance=1 * kOhm(tol=0.1), + ) + ) + (self.dummya,), _ = self.chain(self.dut.a, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.b, self.Block(DummyPassive())) class PowerResistorTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericChipResistor( - resistance=1 * kOhm(tol=0.1), - power=(0, .24)*Watt - )) - (self.dummya, ), _ = self.chain(self.dut.a, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.b, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(GenericChipResistor(resistance=1 * kOhm(tol=0.1), power=(0, 0.24) * Watt)) + (self.dummya,), _ = self.chain(self.dut.a, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.b, self.Block(DummyPassive())) class NonE12ResistorTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(GenericChipResistor( - resistance=8.06 * kOhm(tol=0.01), - )) - (self.dummya, ), _ = self.chain(self.dut.a, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.b, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block( + GenericChipResistor( + resistance=8.06 * kOhm(tol=0.01), + ) + ) + (self.dummya,), _ = self.chain(self.dut.a, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.b, self.Block(DummyPassive())) class ResistorTestCase(unittest.TestCase): - def test_basic_resistor(self) -> None: - compiled = ScalaCompiler.compile(ResistorTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Resistor_SMD:R_0201_0603Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '1k, 1%, 0.05 W') + def test_basic_resistor(self) -> None: + compiled = ScalaCompiler.compile(ResistorTestTop) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Resistor_SMD:R_0201_0603Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "1k, 1%, 0.05 W") - def test_power_resistor(self) -> None: - compiled = ScalaCompiler.compile(PowerResistorTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Resistor_SMD:R_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '1k, 1%, 0.25 W') + def test_power_resistor(self) -> None: + compiled = ScalaCompiler.compile(PowerResistorTestTop) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Resistor_SMD:R_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "1k, 1%, 0.25 W") - def test_non_e12_resistor(self) -> None: - compiled = ScalaCompiler.compile(NonE12ResistorTestTop, Refinements( - instance_values=[(['dut', 'series'], 0)] - )) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Resistor_SMD:R_0201_0603Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '8.06k, 1%, 0.05 W') + def test_non_e12_resistor(self) -> None: + compiled = ScalaCompiler.compile(NonE12ResistorTestTop, Refinements(instance_values=[(["dut", "series"], 0)])) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Resistor_SMD:R_0201_0603Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "8.06k, 1%, 0.05 W") - def test_min_package(self) -> None: - compiled = ScalaCompiler.compile(ResistorTestTop, Refinements( - instance_values=[(['dut', 'footprint_area'], Range.from_lower(4.0))] - )) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Resistor_SMD:R_0603_1608Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '1k, 1%, 0.1 W') + def test_min_package(self) -> None: + compiled = ScalaCompiler.compile( + ResistorTestTop, Refinements(instance_values=[(["dut", "footprint_area"], Range.from_lower(4.0))]) + ) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Resistor_SMD:R_0603_1608Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "1k, 1%, 0.1 W") - def test_footprint(self) -> None: - compiled = ScalaCompiler.compile(ResistorTestTop, Refinements( - instance_values=[(['dut', 'footprint_spec'], 'Resistor_SMD:R_1206_3216Metric')] - )) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Resistor_SMD:R_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_value']), '1k, 1%, 0.25 W') + def test_footprint(self) -> None: + compiled = ScalaCompiler.compile( + ResistorTestTop, + Refinements(instance_values=[(["dut", "footprint_spec"], "Resistor_SMD:R_1206_3216Metric")]), + ) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Resistor_SMD:R_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "fp_value"]), "1k, 1%, 0.25 W") diff --git a/edg/abstract_parts/test_rf.py b/edg/abstract_parts/test_rf.py index e8e0d65a9..0a5ffe682 100644 --- a/edg/abstract_parts/test_rf.py +++ b/edg/abstract_parts/test_rf.py @@ -8,14 +8,14 @@ def test_low_pass_filter(self) -> None: # example 7.2 from https://www.silabs.com/documents/public/application-notes/an1275-imp-match-for-network-arch.pdf # note the results in the app note are NOT numerically accurate (and in some cases just flat out wrong) # overall result using https://www.eeweb.com/tools/pi-match/ - c1, c2, l, rv = PiLowPassFilter._calculate_values(2445e6, 2445/500, 50, complex(21, -1.15)) + c1, c2, l, rv = PiLowPassFilter._calculate_values(2445e6, 2445 / 500, 50, complex(21, -1.15)) self.assertAlmostEqual(rv, 2.007, delta=0.001) self.assertAlmostEqual(l, 1.0414e-9, delta=0.001e-9) self.assertAlmostEqual(c1, 6.366e-12, delta=0.001e-12) self.assertAlmostEqual(c2, 9.353e-12, delta=0.001e-12) # ESP32 example - NOTE this has excessively large Q - c1, c2, l, rv = PiLowPassFilter._calculate_values(2443e6, 2443/82, complex(35, 10), 50) + c1, c2, l, rv = PiLowPassFilter._calculate_values(2443e6, 2443 / 82, complex(35, 10), 50) self.assertAlmostEqual(l, 0.204e-9, delta=0.001e-9) self.assertAlmostEqual(c1, 45.095e-12, delta=0.001e-12) self.assertAlmostEqual(c2, 38.82e-12, delta=0.01e-12) diff --git a/edg/abstract_parts/test_switching_converters.py b/edg/abstract_parts/test_switching_converters.py index 9a193c4ce..2cecd9b09 100644 --- a/edg/abstract_parts/test_switching_converters.py +++ b/edg/abstract_parts/test_switching_converters.py @@ -12,9 +12,15 @@ class SwitchingConverterCalculationTest(unittest.TestCase): def test_buck_converter(self) -> None: values_ref = BuckConverterPowerPath._calculate_parameters( - Range.exact(5), Range.exact(2.5), Range.exact(100e3), Range.exact(1), - Range.exact(1), Range.exact(0.1), 0.01, 0.001, - efficiency=Range.exact(1) + Range.exact(5), + Range.exact(2.5), + Range.exact(100e3), + Range.exact(1), + Range.exact(1), + Range.exact(0.1), + 0.01, + 0.001, + efficiency=Range.exact(1), ) self.assertEqual(values_ref.dutycycle, Range.exact(0.5)) # validated against https://www.omnicalculator.com/physics/buck-converter @@ -22,9 +28,15 @@ def test_buck_converter(self) -> None: # test that component values are calculated for worst-case conversion values = BuckConverterPowerPath._calculate_parameters( - Range(4, 5), Range(2.5, 4), Range.exact(100e3), Range.exact(1), - Range.exact(1), Range.exact(0.1), 0.01, 0.001, - efficiency=Range.exact(1) + Range(4, 5), + Range(2.5, 4), + Range.exact(100e3), + Range.exact(1), + Range.exact(1), + Range.exact(0.1), + 0.01, + 0.001, + efficiency=Range.exact(1), ) self.assertEqual(values_ref.inductance, values.inductance) self.assertEqual(values_ref.input_capacitance, values.input_capacitance) @@ -33,27 +45,45 @@ def test_buck_converter(self) -> None: def test_buck_converter_example(self) -> None: # using the example from https://passive-components.eu/buck-converter-design-and-calculation/ values = BuckConverterPowerPath._calculate_parameters( - Range.exact(12 + 0.4), Range.exact(3.3 + 0.4), Range.exact(500e3), Range.exact(1), - Range.exact(2), Range.exact(0.35), 1, 0.0165, - efficiency=Range.exact(1) + Range.exact(12 + 0.4), + Range.exact(3.3 + 0.4), + Range.exact(500e3), + Range.exact(1), + Range.exact(2), + Range.exact(0.35), + 1, + 0.0165, + efficiency=Range.exact(1), ) self.assertAlmostEqual(values.dutycycle.upper, 0.298, places=3) self.assertAlmostEqual(values.inductance.upper, 14.8e-6, places=7) # the example uses a ripple current of 0.346 for the rest of the calculations values = BuckConverterPowerPath._calculate_parameters( - Range.exact(12 + 0.4), Range.exact(3.3 + 0.4), Range.exact(500e3), Range.exact(1), - Range.exact(2), Range.exact(0.346), 1, 0.0165, - efficiency=Range.exact(1) + Range.exact(12 + 0.4), + Range.exact(3.3 + 0.4), + Range.exact(500e3), + Range.exact(1), + Range.exact(2), + Range.exact(0.346), + 1, + 0.0165, + efficiency=Range.exact(1), ) self.assertAlmostEqual(values.inductor_peak_currents.upper, 1.173, places=3) self.assertAlmostEqual(values.output_capacitance.lower, 5.24e-6, places=7) def test_boost_converter(self) -> None: values_ref = BoostConverterPowerPath._calculate_parameters( - Range.exact(5), Range.exact(10), Range.exact(100e3), Range.exact(0.5), - Range.exact(2), Range.exact(0.4), 0.01, 0.001, - efficiency=Range.exact(1) + Range.exact(5), + Range.exact(10), + Range.exact(100e3), + Range.exact(0.5), + Range.exact(2), + Range.exact(0.4), + 0.01, + 0.001, + efficiency=Range.exact(1), ) self.assertEqual(values_ref.dutycycle, Range.exact(0.5)) # validated against https://www.omnicalculator.com/physics/boost-converter @@ -62,9 +92,15 @@ def test_boost_converter(self) -> None: # test that component values are calculated for worst-case conversion values = BoostConverterPowerPath._calculate_parameters( - Range(5, 8), Range(7, 10), Range.exact(100e3), Range.exact(0.5), - Range.exact(2), Range.exact(0.4), 0.01, 0.001, - efficiency=Range.exact(1) + Range(5, 8), + Range(7, 10), + Range.exact(100e3), + Range.exact(0.5), + Range.exact(2), + Range.exact(0.4), + 0.01, + 0.001, + efficiency=Range.exact(1), ) self.assertEqual(values_ref.inductance, values.inductance) self.assertEqual(values_ref.input_capacitance, values.input_capacitance) @@ -73,9 +109,15 @@ def test_boost_converter(self) -> None: def test_boost_converter_example(self) -> None: # using the example from https://passive-components.eu/boost-converter-design-and-calculation/ values = BoostConverterPowerPath._calculate_parameters( - Range.exact(5), Range.exact(12 + 0.4), Range.exact(500e3), Range.exact(0.5), - Range.exact(2), Range.exact(0.35), 1, 1, - efficiency=Range.exact(1) + Range.exact(5), + Range.exact(12 + 0.4), + Range.exact(500e3), + Range.exact(0.5), + Range.exact(2), + Range.exact(0.35), + 1, + 1, + efficiency=Range.exact(1), ) self.assertAlmostEqual(values.dutycycle.upper, 0.597, places=3) self.assertAlmostEqual(values.inductance.upper, 13.75e-6, places=7) @@ -83,9 +125,15 @@ def test_boost_converter_example(self) -> None: # the example continues with a normalized inductance of 15uH values = BoostConverterPowerPath._calculate_parameters( - Range.exact(5), Range.exact(12 + 0.4), Range.exact(500e3), Range.exact(0.5), - Range.exact(2), Range.exact(0.321), 0.01, 0.06, - efficiency=Range.exact(1) + Range.exact(5), + Range.exact(12 + 0.4), + Range.exact(500e3), + Range.exact(0.5), + Range.exact(2), + Range.exact(0.321), + 0.01, + 0.06, + efficiency=Range.exact(1), ) self.assertAlmostEqual(values.dutycycle.upper, 0.597, places=3) self.assertAlmostEqual(values.inductance.upper, 15.0e-6, places=7) @@ -107,23 +155,28 @@ class TestInductor(Inductor): def contents(self) -> None: super().contents() self.assign(self.actual_inductance, self.inductance) - self.assign(self.actual_current_rating, (0, 1.5)*Amp) + self.assign(self.actual_current_rating, (0, 1.5) * Amp) self.assign(self.actual_frequency_rating, Range.all()) class BuckPowerPathTestTop(DesignTop): def __init__(self) -> None: super().__init__() - self.dut = self.Block(BuckConverterPowerPath( - input_voltage=Range(4, 6), output_voltage=(2, 3), - frequency= Range.exact(100e3), output_current=Range(0.2, 1), - sw_current_limits=Range(0, 2), - input_voltage_ripple=75*mVolt, output_voltage_ripple=25*mVolt, - )) - (self.pwr_in, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_in) - (self.switch, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.switch) - (self.pwr_out, ), _ = self.chain(self.Block(DummyVoltageSink()), self.dut.pwr_out) - (self.gnd, ), _ = self.chain(self.Block(DummyGround()), self.dut.gnd) + self.dut = self.Block( + BuckConverterPowerPath( + input_voltage=Range(4, 6), + output_voltage=(2, 3), + frequency=Range.exact(100e3), + output_current=Range(0.2, 1), + sw_current_limits=Range(0, 2), + input_voltage_ripple=75 * mVolt, + output_voltage_ripple=25 * mVolt, + ) + ) + (self.pwr_in,), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_in) + (self.switch,), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.switch) + (self.pwr_out,), _ = self.chain(self.Block(DummyVoltageSink()), self.dut.pwr_out) + (self.gnd,), _ = self.chain(self.Block(DummyGround()), self.dut.gnd) self.require(self.dut.actual_dutycycle.contains(Range(0.334, 0.832))) self.require(self.dut.actual_inductor_current_ripple.contains(Range(0.433, 0.478))) @@ -137,25 +190,28 @@ def refinements(self) -> Refinements: (Capacitor, TestCapacitor), (Inductor, TestInductor), ], - instance_values=[ - (['dut', 'inductor', 'actual_inductance'], Range.from_tolerance(33e-6, 0.05)) - ] + instance_values=[(["dut", "inductor", "actual_inductance"], Range.from_tolerance(33e-6, 0.05))], ) class BoostPowerPathTestTop(DesignTop): def __init__(self) -> None: super().__init__() - self.dut = self.Block(BoostConverterPowerPath( - input_voltage=Range(4, 6), output_voltage=(10, 14), - frequency=Range.exact(200e3), output_current=Range(0.2, 0.5), - sw_current_limits=Range(0, 2), - input_voltage_ripple=75*mVolt, output_voltage_ripple=25*mVolt, - )) - (self.pwr_in, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_in) - (self.pwr_out, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_out) - (self.switch, ), _ = self.chain(self.Block(DummyVoltageSink()), self.dut.switch) - (self.gnd, ), _ = self.chain(self.Block(DummyGround()), self.dut.gnd) + self.dut = self.Block( + BoostConverterPowerPath( + input_voltage=Range(4, 6), + output_voltage=(10, 14), + frequency=Range.exact(200e3), + output_current=Range(0.2, 0.5), + sw_current_limits=Range(0, 2), + input_voltage_ripple=75 * mVolt, + output_voltage_ripple=25 * mVolt, + ) + ) + (self.pwr_in,), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_in) + (self.pwr_out,), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_out) + (self.switch,), _ = self.chain(self.Block(DummyVoltageSink()), self.dut.switch) + (self.gnd,), _ = self.chain(self.Block(DummyGround()), self.dut.gnd) self.require(self.dut.actual_dutycycle.contains(Range(0.4, 0.771))) self.require(self.dut.actual_inductor_current_ripple.contains(Range(0.495, 0.546))) @@ -169,9 +225,7 @@ def refinements(self) -> Refinements: (Capacitor, TestCapacitor), (Inductor, TestInductor), ], - instance_values=[ - (['dut', 'inductor', 'actual_inductance'], Range.from_tolerance(33e-6, 0.05)) - ] + instance_values=[(["dut", "inductor", "actual_inductance"], Range.from_tolerance(33e-6, 0.05))], ) diff --git a/edg/core/Array.py b/edg/core/Array.py index 0ab555ed1..5b00fbfd8 100644 --- a/edg/core/Array.py +++ b/edg/core/Array.py @@ -2,8 +2,21 @@ import itertools from abc import abstractmethod -from typing import Generic, Any, Tuple, Type, Optional, Union, Iterable, overload, Hashable, List, \ - ItemsView, Callable, Dict +from typing import ( + Generic, + Any, + Tuple, + Type, + Optional, + Union, + Iterable, + overload, + Hashable, + List, + ItemsView, + Callable, + Dict, +) from deprecated import deprecated from typing_extensions import TypeVar, override @@ -19,366 +32,380 @@ class MapExtractBinding(Binding): - def __init__(self, container: BaseVector, elt: ConstraintExpr): - super().__init__() - self.container = container - self.elt = elt + def __init__(self, container: BaseVector, elt: ConstraintExpr): + super().__init__() + self.container = container + self.elt = elt - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return [self.container] + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return [self.container] - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - contained_map = self.container._get_elt_sample()._create_ref_map(edgir.LocalPath()) - pb.map_extract.container.ref.CopyFrom(ref_map[self.container]) # TODO support arbitrary refs - pb.map_extract.path.CopyFrom(contained_map[self.elt]) + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + contained_map = self.container._get_elt_sample()._create_ref_map(edgir.LocalPath()) + pb.map_extract.container.ref.CopyFrom(ref_map[self.container]) # TODO support arbitrary refs + pb.map_extract.path.CopyFrom(contained_map[self.elt]) class FlattenBinding(Binding): - def __init__(self, elts: ConstraintExpr): - super().__init__() - self.elts = elts + def __init__(self, elts: ConstraintExpr): + super().__init__() + self.elts = elts - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return [self.elts] + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return [self.elts] - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.unary_set.op = edgir.UnarySetExpr.Op.FLATTEN - self.elts._populate_expr_proto(pb.unary_set.vals, ref_map) + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.unary_set.op = edgir.UnarySetExpr.Op.FLATTEN + self.elts._populate_expr_proto(pb.unary_set.vals, ref_map) @non_library class BaseVector(BaseContainerPort): - @abstractmethod - def _get_elt_sample(self) -> BasePort: - ... + @abstractmethod + def _get_elt_sample(self) -> BasePort: ... # A 'fake'/'intermediate'/'view' vector object used as a return in map_extract operations. -VectorType = TypeVar('VectorType', bound=Port, default=Port) +VectorType = TypeVar("VectorType", bound=Port, default=Port) + + @non_library class DerivedVector(BaseVector, Generic[VectorType]): - # TODO: Library types need to be removed from the type hierarchy, because this does not generate into a library elt - def __init__(self, base: BaseVector, target: VectorType) -> None: - if not isinstance(base, BaseVector): - raise TypeError(f"base of DerivedVector(...) must be BaseVector, got {base} of type {type(base)}") - if not isinstance(target, BasePort): - raise TypeError(f"target of DerivedVector(...) must be BasePort, got {target} of type {type(target)}") + # TODO: Library types need to be removed from the type hierarchy, because this does not generate into a library elt + def __init__(self, base: BaseVector, target: VectorType) -> None: + if not isinstance(base, BaseVector): + raise TypeError(f"base of DerivedVector(...) must be BaseVector, got {base} of type {type(base)}") + if not isinstance(target, BasePort): + raise TypeError(f"target of DerivedVector(...) must be BasePort, got {target} of type {type(target)}") - super().__init__() + super().__init__() - assert base._is_bound() - assert target._is_bound() # TODO check target in base, TODO check is elt sample type - self.base = base - self.target = target - assert base._parent is not None # to satisfy type checker, though kind of duplicates _is_bound - self._bind_in_place(base._parent) + assert base._is_bound() + assert target._is_bound() # TODO check target in base, TODO check is elt sample type + self.base = base + self.target = target + assert base._parent is not None # to satisfy type checker, though kind of duplicates _is_bound + self._bind_in_place(base._parent) - @override - def _type_of(self) -> Hashable: - return (self.target._type_of(),) + @override + def _type_of(self) -> Hashable: + return (self.target._type_of(),) - @override - def _get_elt_sample(self) -> BasePort: - return self.target + @override + def _get_elt_sample(self) -> BasePort: + return self.target - @override - def _populate_portlike_proto(self, pb: edgir.PortLike) -> None: - raise RuntimeError() # this can't be a block's port + @override + def _populate_portlike_proto(self, pb: edgir.PortLike) -> None: + raise RuntimeError() # this can't be a block's port - @override - def _def_to_proto(self) -> edgir.PortTypes: - raise RuntimeError() # this doesn't generate into a library element + @override + def _def_to_proto(self) -> edgir.PortTypes: + raise RuntimeError() # this doesn't generate into a library element - @override - def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: - raise RuntimeError() # should never happen + @override + def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: + raise RuntimeError() # should never happen # An 'elastic' array of ports type, with unspecified length at declaration time, and length # determined by connections in the parent block. @non_library class Vector(BaseVector, Generic[VectorType]): - # TODO: Library types need to be removed from the type hierarchy, because this does not generate into a library elt - def __init__(self, tpe: VectorType) -> None: - if not isinstance(tpe, BasePort): - raise EdgTypeError(f"arg to Vector(...)", tpe, BasePort) - - super().__init__() - - assert not tpe._is_bound() - self._tpe = tpe - self._elt_sample = tpe._bind(self) - self._elts: Optional[Dict[str, VectorType]] = None # concrete elements, for boundary ports - self._elt_next_index = 0 - self._requests: List[Tuple[Optional[str], BasePort]] = [] # used to track request / request_vector for ref_map - - self._length = IntExpr()._bind(LengthBinding(self)) - self._requested = ArrayStringExpr()._bind(AllocatedBinding(self)) - - @override - def __repr__(self) -> str: - # TODO dedup w/ Core.__repr__ - # but this can't depend on get_def_name since that crashes - return "Array[%s]@%02x" % (self._elt_sample, (id(self) // 4) & 0xff) - - def __getitem__(self, item: str) -> VectorType: - """Returns a port previously defined by append_elt, indexed by (required) suggested_name. - Can only be called from the block defining this port (where this is a boundary port), - and this port must be bound.""" - assert self._is_bound(), "not bound, can't create array elements" - assert builder.get_enclosing_block() is self._block_parent(), "can only create elts in block parent of array" - assert self._elts is not None, "no elts defined" - return self._elts[item] - - def items(self) -> ItemsView[str, VectorType]: - assert self._elts is not None, "no elts defined" - return self._elts.items() - - # unlike most other LibraryElement types, the names are stored in _elts and _allocates - @override - def _name_of_child(self, subelt: Any, context: Any, allow_unknown: bool = False) -> str: - from .HierarchyBlock import Block - block_parent = self._block_parent() - assert isinstance(block_parent, Block) - - if context is block_parent: - # in block defining this port (direct elt definition), or in test top - assert self._elts is not None, "can't get name on undefined vector" - for (name, elt) in self._elts.items(): - if subelt is elt: - return name - if allow_unknown: - return f"(unknown {subelt.__class__.__name__})" - else: - raise ValueError(f"no name for {subelt}") - elif context is block_parent._parent: - # in block enclosing the block defining this port (allocate required) - for (i, (suggested_name, allocate_elt)) in enumerate(self._requests): - if subelt is allocate_elt: - if suggested_name is not None: - return suggested_name - else: - return f"_allocate_{i}" - if allow_unknown: - return f"(unknown {subelt.__class__.__name__})" - else: - raise ValueError(f"allocated elt not found {subelt}") - else: - raise ValueError(f"unknown context of array") - - @override - def _get_elt_sample(self) -> BasePort: - return self._elt_sample - - @override - def _get_def_name(self) -> str: - raise RuntimeError() # this doesn't generate into a library element - - @override - def _populate_portlike_proto(self, pb: edgir.PortLike) -> None: - pb.array.self_class.target.name = self._elt_sample._get_def_name() - if self._elts is not None: - pb.array.ports.SetInParent() # mark as defined, even if empty - for name, elt in self._elts.items(): - elt._populate_portlike_proto(edgir.add_pair(pb.array.ports.ports, name)) - - @override - def _def_to_proto(self) -> edgir.PortTypes: - raise RuntimeError() # this doesn't generate into a library element - - @override - def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: - super()._build_ref_map(ref_map, prefix) - ref_map[self._length] = edgir.localpath_concat(prefix, edgir.LENGTH) - ref_map[self._requested] = edgir.localpath_concat(prefix, edgir.ALLOCATED) - elts_items = self._elts.items() if self._elts is not None else [] - for index, elt in elts_items: - elt._build_ref_map(ref_map, edgir.localpath_concat(prefix, index)) - for suggested_name, request in self._requests: - request._build_ref_map(ref_map, edgir.localpath_concat(prefix, edgir.Allocate(suggested_name))) - - @override - def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: - if self._elts is not None: - return list(itertools.chain(*[ - elt._get_initializers(path_prefix + [name]) for (name, elt) in self._elts.items()])) - else: - return [] - - def defined(self) -> None: - """Marks this vector as defined, even if it is empty. Can be called multiple times, and append_elt can continue - to be used. - Can only be called from the block defining this port (where this is a boundary port), - and this port must be bound.""" - assert self._is_bound(), "not bound, can't create array elements" - assert builder.get_enclosing_block() is self._block_parent(), "can only create elts in block parent of array" - - if self._elts is None: - self._elts = {} - - def append_elt(self, tpe: VectorType, suggested_name: Optional[str] = None) -> VectorType: - """Appends a new element of this array (if this is not to be a dynamically-sized array - including - when subclassing a base class with a dynamically-sized array) with either the number of elements - or with specific names of elements. - Argument is the port model (optionally with initializers) and an optional suggested name. - Can only be called from the block defining this port (where this is a boundary port), - and this port must be bound.""" - assert self._is_bound(), "not bound, can't create array elements" - assert builder.get_enclosing_block() is self._block_parent(), "can only create elts in block parent of array" - assert type(tpe) is type(self._tpe), f"created elts {type(tpe)} must be same type as array type {type(self._tpe)}" - - if self._elts is None: - self._elts = {} - if suggested_name is None: - suggested_name = str(self._elt_next_index) - self._elt_next_index += 1 - assert suggested_name not in self._elts, f"duplicate Port Vector element name {suggested_name}" - - self._elts[suggested_name] = tpe._bind(self) - return self._elts[suggested_name] - - @deprecated(reason="renamed to request") - def allocate(self, suggested_name: Optional[str] = None) -> VectorType: - return self.request(suggested_name) - - def request(self, suggested_name: Optional[str] = None) -> VectorType: - """Returns a new port of this Vector. - Can only be called from the block containing the block containing this as a port (used to allocate a - port of an internal block). - To create elements where this is the boundary block, use init_elts(...). - """ - from .HierarchyBlock import Block - assert self._is_bound(), "not bound, can't allocate array elements" - block_parent = self._block_parent() - assert isinstance(block_parent, Block), "can only allocate from ports of a Block" - assert builder.get_enclosing_block() is block_parent._parent or builder.get_enclosing_block() is None, \ - "can only allocate ports of internal blocks" # None case is to allow elaborating in unit tests - # self._elts is ignored, since that defines the inner-facing behavior, which this is outer-facing behavior - allocated = type(self._tpe).empty()._bind(self) - self._requests.append((suggested_name, allocated)) - return allocated - - @deprecated(reason="renamed to request_vector") - def allocate_vector(self, suggested_name: Optional[str] = None) -> Vector[VectorType]: - return self.request_vector(suggested_name) - - def request_vector(self, suggested_name: Optional[str] = None) -> Vector[VectorType]: - """Returns a new dynamic-length, array-port slice of this Vector. - Can only be called from the block containing the block containing this as a port (used to allocate a - port of an internal block). - Can only be used as an array elements sink. - """ - from .HierarchyBlock import Block - assert self._is_bound(), "not bound, can't allocate array elements" - block_parent = self._block_parent() - assert isinstance(block_parent, Block), "can only allocate from ports of a Block" - assert builder.get_enclosing_block() is block_parent._parent or builder.get_enclosing_block() is None, \ - "can only allocate ports of internal blocks" # None case is to allow elaborating in unit tests - # self._elts is ignored, since that defines the inner-facing behavior, which this is outer-facing behavior - allocated = Vector(type(self._tpe).empty())._bind(self) - self._requests.append((suggested_name, allocated)) - return allocated - - def length(self) -> IntExpr: - return self._length - - @deprecated(reason="renamed to requested") - def allocated(self) -> ArrayStringExpr: - return self.requested() - - def requested(self) -> ArrayStringExpr: - return self._requested - - @override - def _type_of(self) -> Hashable: - return (self._elt_sample._type_of(),) - - def elt_type(self) -> Type[VectorType]: - """Returns the type of the element.""" - return type(self._elt_sample) - - SelectorType = TypeVar('SelectorType', bound=ConstraintExpr) - @staticmethod - def validate_selector(expected: Type[SelectorType], result: ConstraintExpr) -> SelectorType: - # TODO check returned type is child - if not isinstance(result, expected): - raise EdgTypeError(f"selector return", result, expected) - return result - - ExtractPortType = TypeVar('ExtractPortType', bound=Port) - # See the note in ArrayExpr for why this is expanded. - @overload - def map_extract(self, selector: Callable[[VectorType], BoolExpr]) -> ArrayBoolExpr: ... - @overload - def map_extract(self, selector: Callable[[VectorType], IntExpr]) -> ArrayIntExpr: ... - @overload - def map_extract(self, selector: Callable[[VectorType], FloatExpr]) -> ArrayFloatExpr: ... - @overload - def map_extract(self, selector: Callable[[VectorType], RangeExpr]) -> ArrayRangeExpr: ... - @overload - def map_extract(self, selector: Callable[[VectorType], StringExpr]) -> ArrayStringExpr: ... - @overload - def map_extract(self, selector: Callable[[VectorType], ExtractPortType]) -> DerivedVector[ExtractPortType]: ... - - def map_extract(self, selector: Callable[[VectorType], Union[ConstraintExpr, ExtractPortType]]) -> Union[ArrayExpr, DerivedVector[ExtractPortType]]: - param = selector(self._elt_sample) - if isinstance(param, ConstraintExpr): # TODO check that returned type is child - return ArrayExpr.array_of_elt(param)._bind(MapExtractBinding(self, param)) - elif isinstance(param, Port): - return DerivedVector(self, param) - else: - raise EdgTypeError(f"selector return", param, (ConstraintExpr, Port)) - - def any_connected(self) -> BoolExpr: - return self.any(lambda port: port.is_connected()) - - def any(self, selector: Callable[[VectorType], BoolExpr]) -> BoolExpr: - param = self.validate_selector(BoolExpr, selector(self._elt_sample)) - return ArrayBoolExpr()._bind(MapExtractBinding(self, param)).any() - - def all(self, selector: Callable[[VectorType], BoolExpr]) -> BoolExpr: - param = self.validate_selector(BoolExpr, selector(self._elt_sample)) - return ArrayBoolExpr()._bind(MapExtractBinding(self, param)).all() - - def count(self, selector: Callable[[VectorType], BoolExpr]) -> IntExpr: - param = self.validate_selector(BoolExpr, selector(self._elt_sample)) - return ArrayBoolExpr()._bind(MapExtractBinding(self, param)).count() - - @overload - def sum(self, selector: Callable[[VectorType], RangeExpr]) -> RangeExpr: ... - @overload - def sum(self, selector: Callable[[VectorType], FloatExpr]) -> FloatExpr: ... - def sum(self, selector: Callable[[VectorType], Union[RangeExpr, FloatExpr]]) -> Union[RangeExpr, FloatExpr]: - param = selector(self._elt_sample) - if isinstance(param, FloatExpr): - return ArrayFloatExpr()._bind(MapExtractBinding(self, param)).sum() - elif isinstance(param, RangeExpr): - return ArrayRangeExpr()._bind(MapExtractBinding(self, param)).sum() - else: # TODO check that returned type is child - raise EdgTypeError(f"selector return", param, (FloatExpr, RangeExpr)) - - def min(self, selector: Callable[[VectorType], FloatExpr]) -> FloatExpr: - param = self.validate_selector(FloatExpr, selector(self._elt_sample)) - return ArrayFloatExpr()._bind(MapExtractBinding(self, param)).min() - - def max(self, selector: Callable[[VectorType], FloatExpr]) -> FloatExpr: - param = self.validate_selector(FloatExpr, selector(self._elt_sample)) - return ArrayFloatExpr()._bind(MapExtractBinding(self, param)).max() - - def intersection(self, selector: Callable[[VectorType], RangeExpr]) -> RangeExpr: - param = self.validate_selector(RangeExpr, selector(self._elt_sample)) - return ArrayRangeExpr()._bind(MapExtractBinding(self, param)).intersection() - - def hull(self, selector: Callable[[VectorType], RangeExpr]) -> RangeExpr: - param = self.validate_selector(RangeExpr, selector(self._elt_sample)) - return ArrayRangeExpr()._bind(MapExtractBinding(self, param)).hull() - - ArrayType = TypeVar('ArrayType', bound=ArrayExpr) - def flatten(self, selector: Callable[[VectorType], ArrayType]) -> ArrayType: - param = self.validate_selector(ArrayExpr, selector(self._elt_sample)) - array_of_arrays = ArrayExpr.array_of_elt(param._elt_sample)._bind(MapExtractBinding(self, param)) - return ArrayExpr.array_of_elt(param._elt_sample)._bind(FlattenBinding(array_of_arrays)) # type: ignore + # TODO: Library types need to be removed from the type hierarchy, because this does not generate into a library elt + def __init__(self, tpe: VectorType) -> None: + if not isinstance(tpe, BasePort): + raise EdgTypeError(f"arg to Vector(...)", tpe, BasePort) + + super().__init__() + + assert not tpe._is_bound() + self._tpe = tpe + self._elt_sample = tpe._bind(self) + self._elts: Optional[Dict[str, VectorType]] = None # concrete elements, for boundary ports + self._elt_next_index = 0 + self._requests: List[Tuple[Optional[str], BasePort]] = [] # used to track request / request_vector for ref_map + + self._length = IntExpr()._bind(LengthBinding(self)) + self._requested = ArrayStringExpr()._bind(AllocatedBinding(self)) + + @override + def __repr__(self) -> str: + # TODO dedup w/ Core.__repr__ + # but this can't depend on get_def_name since that crashes + return "Array[%s]@%02x" % (self._elt_sample, (id(self) // 4) & 0xFF) + + def __getitem__(self, item: str) -> VectorType: + """Returns a port previously defined by append_elt, indexed by (required) suggested_name. + Can only be called from the block defining this port (where this is a boundary port), + and this port must be bound.""" + assert self._is_bound(), "not bound, can't create array elements" + assert builder.get_enclosing_block() is self._block_parent(), "can only create elts in block parent of array" + assert self._elts is not None, "no elts defined" + return self._elts[item] + + def items(self) -> ItemsView[str, VectorType]: + assert self._elts is not None, "no elts defined" + return self._elts.items() + + # unlike most other LibraryElement types, the names are stored in _elts and _allocates + @override + def _name_of_child(self, subelt: Any, context: Any, allow_unknown: bool = False) -> str: + from .HierarchyBlock import Block + + block_parent = self._block_parent() + assert isinstance(block_parent, Block) + + if context is block_parent: + # in block defining this port (direct elt definition), or in test top + assert self._elts is not None, "can't get name on undefined vector" + for name, elt in self._elts.items(): + if subelt is elt: + return name + if allow_unknown: + return f"(unknown {subelt.__class__.__name__})" + else: + raise ValueError(f"no name for {subelt}") + elif context is block_parent._parent: + # in block enclosing the block defining this port (allocate required) + for i, (suggested_name, allocate_elt) in enumerate(self._requests): + if subelt is allocate_elt: + if suggested_name is not None: + return suggested_name + else: + return f"_allocate_{i}" + if allow_unknown: + return f"(unknown {subelt.__class__.__name__})" + else: + raise ValueError(f"allocated elt not found {subelt}") + else: + raise ValueError(f"unknown context of array") + + @override + def _get_elt_sample(self) -> BasePort: + return self._elt_sample + + @override + def _get_def_name(self) -> str: + raise RuntimeError() # this doesn't generate into a library element + + @override + def _populate_portlike_proto(self, pb: edgir.PortLike) -> None: + pb.array.self_class.target.name = self._elt_sample._get_def_name() + if self._elts is not None: + pb.array.ports.SetInParent() # mark as defined, even if empty + for name, elt in self._elts.items(): + elt._populate_portlike_proto(edgir.add_pair(pb.array.ports.ports, name)) + + @override + def _def_to_proto(self) -> edgir.PortTypes: + raise RuntimeError() # this doesn't generate into a library element + + @override + def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: + super()._build_ref_map(ref_map, prefix) + ref_map[self._length] = edgir.localpath_concat(prefix, edgir.LENGTH) + ref_map[self._requested] = edgir.localpath_concat(prefix, edgir.ALLOCATED) + elts_items = self._elts.items() if self._elts is not None else [] + for index, elt in elts_items: + elt._build_ref_map(ref_map, edgir.localpath_concat(prefix, index)) + for suggested_name, request in self._requests: + request._build_ref_map(ref_map, edgir.localpath_concat(prefix, edgir.Allocate(suggested_name))) + + @override + def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: + if self._elts is not None: + return list( + itertools.chain(*[elt._get_initializers(path_prefix + [name]) for (name, elt) in self._elts.items()]) + ) + else: + return [] + + def defined(self) -> None: + """Marks this vector as defined, even if it is empty. Can be called multiple times, and append_elt can continue + to be used. + Can only be called from the block defining this port (where this is a boundary port), + and this port must be bound.""" + assert self._is_bound(), "not bound, can't create array elements" + assert builder.get_enclosing_block() is self._block_parent(), "can only create elts in block parent of array" + + if self._elts is None: + self._elts = {} + + def append_elt(self, tpe: VectorType, suggested_name: Optional[str] = None) -> VectorType: + """Appends a new element of this array (if this is not to be a dynamically-sized array - including + when subclassing a base class with a dynamically-sized array) with either the number of elements + or with specific names of elements. + Argument is the port model (optionally with initializers) and an optional suggested name. + Can only be called from the block defining this port (where this is a boundary port), + and this port must be bound.""" + assert self._is_bound(), "not bound, can't create array elements" + assert builder.get_enclosing_block() is self._block_parent(), "can only create elts in block parent of array" + assert type(tpe) is type( + self._tpe + ), f"created elts {type(tpe)} must be same type as array type {type(self._tpe)}" + + if self._elts is None: + self._elts = {} + if suggested_name is None: + suggested_name = str(self._elt_next_index) + self._elt_next_index += 1 + assert suggested_name not in self._elts, f"duplicate Port Vector element name {suggested_name}" + + self._elts[suggested_name] = tpe._bind(self) + return self._elts[suggested_name] + + @deprecated(reason="renamed to request") + def allocate(self, suggested_name: Optional[str] = None) -> VectorType: + return self.request(suggested_name) + + def request(self, suggested_name: Optional[str] = None) -> VectorType: + """Returns a new port of this Vector. + Can only be called from the block containing the block containing this as a port (used to allocate a + port of an internal block). + To create elements where this is the boundary block, use init_elts(...). + """ + from .HierarchyBlock import Block + + assert self._is_bound(), "not bound, can't allocate array elements" + block_parent = self._block_parent() + assert isinstance(block_parent, Block), "can only allocate from ports of a Block" + assert ( + builder.get_enclosing_block() is block_parent._parent or builder.get_enclosing_block() is None + ), "can only allocate ports of internal blocks" # None case is to allow elaborating in unit tests + # self._elts is ignored, since that defines the inner-facing behavior, which this is outer-facing behavior + allocated = type(self._tpe).empty()._bind(self) + self._requests.append((suggested_name, allocated)) + return allocated + + @deprecated(reason="renamed to request_vector") + def allocate_vector(self, suggested_name: Optional[str] = None) -> Vector[VectorType]: + return self.request_vector(suggested_name) + + def request_vector(self, suggested_name: Optional[str] = None) -> Vector[VectorType]: + """Returns a new dynamic-length, array-port slice of this Vector. + Can only be called from the block containing the block containing this as a port (used to allocate a + port of an internal block). + Can only be used as an array elements sink. + """ + from .HierarchyBlock import Block + + assert self._is_bound(), "not bound, can't allocate array elements" + block_parent = self._block_parent() + assert isinstance(block_parent, Block), "can only allocate from ports of a Block" + assert ( + builder.get_enclosing_block() is block_parent._parent or builder.get_enclosing_block() is None + ), "can only allocate ports of internal blocks" # None case is to allow elaborating in unit tests + # self._elts is ignored, since that defines the inner-facing behavior, which this is outer-facing behavior + allocated = Vector(type(self._tpe).empty())._bind(self) + self._requests.append((suggested_name, allocated)) + return allocated + + def length(self) -> IntExpr: + return self._length + + @deprecated(reason="renamed to requested") + def allocated(self) -> ArrayStringExpr: + return self.requested() + + def requested(self) -> ArrayStringExpr: + return self._requested + + @override + def _type_of(self) -> Hashable: + return (self._elt_sample._type_of(),) + + def elt_type(self) -> Type[VectorType]: + """Returns the type of the element.""" + return type(self._elt_sample) + + SelectorType = TypeVar("SelectorType", bound=ConstraintExpr) + + @staticmethod + def validate_selector(expected: Type[SelectorType], result: ConstraintExpr) -> SelectorType: + # TODO check returned type is child + if not isinstance(result, expected): + raise EdgTypeError(f"selector return", result, expected) + return result + + ExtractPortType = TypeVar("ExtractPortType", bound=Port) + + # See the note in ArrayExpr for why this is expanded. + @overload + def map_extract(self, selector: Callable[[VectorType], BoolExpr]) -> ArrayBoolExpr: ... + @overload + def map_extract(self, selector: Callable[[VectorType], IntExpr]) -> ArrayIntExpr: ... + @overload + def map_extract(self, selector: Callable[[VectorType], FloatExpr]) -> ArrayFloatExpr: ... + @overload + def map_extract(self, selector: Callable[[VectorType], RangeExpr]) -> ArrayRangeExpr: ... + @overload + def map_extract(self, selector: Callable[[VectorType], StringExpr]) -> ArrayStringExpr: ... + @overload + def map_extract(self, selector: Callable[[VectorType], ExtractPortType]) -> DerivedVector[ExtractPortType]: ... + + def map_extract( + self, selector: Callable[[VectorType], Union[ConstraintExpr, ExtractPortType]] + ) -> Union[ArrayExpr, DerivedVector[ExtractPortType]]: + param = selector(self._elt_sample) + if isinstance(param, ConstraintExpr): # TODO check that returned type is child + return ArrayExpr.array_of_elt(param)._bind(MapExtractBinding(self, param)) + elif isinstance(param, Port): + return DerivedVector(self, param) + else: + raise EdgTypeError(f"selector return", param, (ConstraintExpr, Port)) + + def any_connected(self) -> BoolExpr: + return self.any(lambda port: port.is_connected()) + + def any(self, selector: Callable[[VectorType], BoolExpr]) -> BoolExpr: + param = self.validate_selector(BoolExpr, selector(self._elt_sample)) + return ArrayBoolExpr()._bind(MapExtractBinding(self, param)).any() + + def all(self, selector: Callable[[VectorType], BoolExpr]) -> BoolExpr: + param = self.validate_selector(BoolExpr, selector(self._elt_sample)) + return ArrayBoolExpr()._bind(MapExtractBinding(self, param)).all() + + def count(self, selector: Callable[[VectorType], BoolExpr]) -> IntExpr: + param = self.validate_selector(BoolExpr, selector(self._elt_sample)) + return ArrayBoolExpr()._bind(MapExtractBinding(self, param)).count() + + @overload + def sum(self, selector: Callable[[VectorType], RangeExpr]) -> RangeExpr: ... + @overload + def sum(self, selector: Callable[[VectorType], FloatExpr]) -> FloatExpr: ... + def sum(self, selector: Callable[[VectorType], Union[RangeExpr, FloatExpr]]) -> Union[RangeExpr, FloatExpr]: + param = selector(self._elt_sample) + if isinstance(param, FloatExpr): + return ArrayFloatExpr()._bind(MapExtractBinding(self, param)).sum() + elif isinstance(param, RangeExpr): + return ArrayRangeExpr()._bind(MapExtractBinding(self, param)).sum() + else: # TODO check that returned type is child + raise EdgTypeError(f"selector return", param, (FloatExpr, RangeExpr)) + + def min(self, selector: Callable[[VectorType], FloatExpr]) -> FloatExpr: + param = self.validate_selector(FloatExpr, selector(self._elt_sample)) + return ArrayFloatExpr()._bind(MapExtractBinding(self, param)).min() + + def max(self, selector: Callable[[VectorType], FloatExpr]) -> FloatExpr: + param = self.validate_selector(FloatExpr, selector(self._elt_sample)) + return ArrayFloatExpr()._bind(MapExtractBinding(self, param)).max() + + def intersection(self, selector: Callable[[VectorType], RangeExpr]) -> RangeExpr: + param = self.validate_selector(RangeExpr, selector(self._elt_sample)) + return ArrayRangeExpr()._bind(MapExtractBinding(self, param)).intersection() + + def hull(self, selector: Callable[[VectorType], RangeExpr]) -> RangeExpr: + param = self.validate_selector(RangeExpr, selector(self._elt_sample)) + return ArrayRangeExpr()._bind(MapExtractBinding(self, param)).hull() + + ArrayType = TypeVar("ArrayType", bound=ArrayExpr) + + def flatten(self, selector: Callable[[VectorType], ArrayType]) -> ArrayType: + param = self.validate_selector(ArrayExpr, selector(self._elt_sample)) + array_of_arrays = ArrayExpr.array_of_elt(param._elt_sample)._bind(MapExtractBinding(self, param)) + return ArrayExpr.array_of_elt(param._elt_sample)._bind(FlattenBinding(array_of_arrays)) # type: ignore diff --git a/edg/core/ArrayExpr.py b/edg/core/ArrayExpr.py index fabcde16a..d564508d1 100644 --- a/edg/core/ArrayExpr.py +++ b/edg/core/ArrayExpr.py @@ -5,9 +5,23 @@ from typing_extensions import TypeVar, override from .Binding import EqOp, ArrayBinding, UnarySetOpBinding, BinarySetOpBinding -from .ConstraintExpr import ConstraintExpr, IntLike, FloatExpr, FloatLike, RangeExpr, RangeLike, \ - BoolExpr, BoolLike, StringLike, \ - NumericOp, BoolOp, RangeSetOp, Binding, StringExpr, IntExpr +from .ConstraintExpr import ( + ConstraintExpr, + IntLike, + FloatExpr, + FloatLike, + RangeExpr, + RangeLike, + BoolExpr, + BoolLike, + StringLike, + NumericOp, + BoolOp, + RangeSetOp, + Binding, + StringExpr, + IntExpr, +) from .Core import Refable from .Ports import BasePort from .Range import Range @@ -15,145 +29,159 @@ class SampleElementBinding(Binding): - def __init__(self) -> None: - super().__init__() - - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: # element should be returned by the containing ConstraintExpr - return [] - - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - raise ValueError # can't be used directly - - -SelfType = TypeVar('SelfType', bound='ArrayExpr') -ArrayEltType = TypeVar('ArrayEltType', bound=ConstraintExpr, covariant=True, default=ConstraintExpr) -ArrayWrappedType = TypeVar('ArrayWrappedType', covariant=True, default=Any) -ArrayCastableType = TypeVar('ArrayCastableType', contravariant=True, default=Any) -class ArrayExpr(ConstraintExpr[ArrayWrappedType, ArrayCastableType], - Generic[ArrayEltType, ArrayWrappedType, ArrayCastableType]): - """An Array-valued ConstraintExpr (a variable-sized list of ConstraintExpr). - All the cases are explicitly expanded (eg, ArrayBoolExpr, ArrayIntExpr, ...) because ConstraintExpr requires the - wrapped-type and castable-type (for example, used in Generator.generator(...) args), yet ArrayExpr also needs the - element type, and there doesn't seem to be any way to express that these are related. So all three are provided, - explicitly in the subclasses. - """ - _elt_type: Type[ArrayEltType] - - @classmethod - @override - def _from_lit(cls, pb: edgir.ValueLit) -> ArrayWrappedType: - assert pb.HasField('array') - return [cls._elt_type._from_lit(sub_pb) for sub_pb in pb.array.elts] # type: ignore - - @classmethod - @override - def _to_expr_type(cls: Type[SelfType], input: ArrayCastableType) -> SelfType: - if isinstance(input, cls): - assert input._is_bound() - return input - elif isinstance(input, list): - elts = [cls._elt_type._to_expr_type(elt) for elt in input] - return cls()._bind(ArrayBinding(elts)) - else: - raise TypeError(f"arg to {cls.__name__} must be ArrayCastableType, got {input} of type {type(input)}") - - @classmethod - @override - def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: - cls._elt_type._populate_decl_proto(pb.array) - - @staticmethod - def array_of_elt(elt: ConstraintExpr) -> ArrayExpr: - """Returns the ArrayExpr type that wraps some element expr.""" - if isinstance(elt, BoolExpr): - return ArrayBoolExpr() - elif isinstance(elt, IntExpr): - return ArrayIntExpr() - elif isinstance(elt, FloatExpr): - return ArrayFloatExpr() - elif isinstance(elt, RangeExpr): - return ArrayRangeExpr() - elif isinstance(elt, StringExpr): - return ArrayStringExpr() - else: - raise TypeError(f"unknown ConstraintExpr type for wrapped param {elt}") - - def __init__(self: SelfType, initializer: Optional[Union[SelfType, ArrayCastableType]]=None) -> None: - super().__init__(initializer) - self._elt_sample: ArrayEltType = self._elt_type()._new_bind(SampleElementBinding()) - - def _create_unary_set_op(self, op: Union[NumericOp, BoolOp, RangeSetOp, EqOp]) -> ArrayEltType: - return self._elt_type._new_bind(UnarySetOpBinding(self, op)) - - def all_unique(self) -> BoolExpr: - return BoolExpr()._new_bind(UnarySetOpBinding(self, EqOp.all_unique)) - - def all_equal(self) -> BoolExpr: - return BoolExpr()._new_bind(UnarySetOpBinding(self, EqOp.all_equal)) - - def sum(self) -> ArrayEltType: - return self._create_unary_set_op(NumericOp.sum) - - def min(self) -> FloatExpr: - return FloatExpr()._new_bind(UnarySetOpBinding(self, RangeSetOp.min)) - - def max(self) -> FloatExpr: - return FloatExpr()._new_bind(UnarySetOpBinding(self, RangeSetOp.max)) - - def intersection(self) -> ArrayEltType: - return self._create_unary_set_op(RangeSetOp.intersection) - - def hull(self) -> ArrayEltType: - return self._create_unary_set_op(RangeSetOp.hull) - - -ArrayBoolLike = Union['ArrayBoolExpr', Sequence[BoolLike]] + def __init__(self) -> None: + super().__init__() + + @override + def get_subexprs( + self, + ) -> Iterable[Union[ConstraintExpr, BasePort]]: # element should be returned by the containing ConstraintExpr + return [] + + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + raise ValueError # can't be used directly + + +SelfType = TypeVar("SelfType", bound="ArrayExpr") +ArrayEltType = TypeVar("ArrayEltType", bound=ConstraintExpr, covariant=True, default=ConstraintExpr) +ArrayWrappedType = TypeVar("ArrayWrappedType", covariant=True, default=Any) +ArrayCastableType = TypeVar("ArrayCastableType", contravariant=True, default=Any) + + +class ArrayExpr( + ConstraintExpr[ArrayWrappedType, ArrayCastableType], Generic[ArrayEltType, ArrayWrappedType, ArrayCastableType] +): + """An Array-valued ConstraintExpr (a variable-sized list of ConstraintExpr). + All the cases are explicitly expanded (eg, ArrayBoolExpr, ArrayIntExpr, ...) because ConstraintExpr requires the + wrapped-type and castable-type (for example, used in Generator.generator(...) args), yet ArrayExpr also needs the + element type, and there doesn't seem to be any way to express that these are related. So all three are provided, + explicitly in the subclasses. + """ + + _elt_type: Type[ArrayEltType] + + @classmethod + @override + def _from_lit(cls, pb: edgir.ValueLit) -> ArrayWrappedType: + assert pb.HasField("array") + return [cls._elt_type._from_lit(sub_pb) for sub_pb in pb.array.elts] # type: ignore + + @classmethod + @override + def _to_expr_type(cls: Type[SelfType], input: ArrayCastableType) -> SelfType: + if isinstance(input, cls): + assert input._is_bound() + return input + elif isinstance(input, list): + elts = [cls._elt_type._to_expr_type(elt) for elt in input] + return cls()._bind(ArrayBinding(elts)) + else: + raise TypeError(f"arg to {cls.__name__} must be ArrayCastableType, got {input} of type {type(input)}") + + @classmethod + @override + def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: + cls._elt_type._populate_decl_proto(pb.array) + + @staticmethod + def array_of_elt(elt: ConstraintExpr) -> ArrayExpr: + """Returns the ArrayExpr type that wraps some element expr.""" + if isinstance(elt, BoolExpr): + return ArrayBoolExpr() + elif isinstance(elt, IntExpr): + return ArrayIntExpr() + elif isinstance(elt, FloatExpr): + return ArrayFloatExpr() + elif isinstance(elt, RangeExpr): + return ArrayRangeExpr() + elif isinstance(elt, StringExpr): + return ArrayStringExpr() + else: + raise TypeError(f"unknown ConstraintExpr type for wrapped param {elt}") + + def __init__(self: SelfType, initializer: Optional[Union[SelfType, ArrayCastableType]] = None) -> None: + super().__init__(initializer) + self._elt_sample: ArrayEltType = self._elt_type()._new_bind(SampleElementBinding()) + + def _create_unary_set_op(self, op: Union[NumericOp, BoolOp, RangeSetOp, EqOp]) -> ArrayEltType: + return self._elt_type._new_bind(UnarySetOpBinding(self, op)) + + def all_unique(self) -> BoolExpr: + return BoolExpr()._new_bind(UnarySetOpBinding(self, EqOp.all_unique)) + + def all_equal(self) -> BoolExpr: + return BoolExpr()._new_bind(UnarySetOpBinding(self, EqOp.all_equal)) + + def sum(self) -> ArrayEltType: + return self._create_unary_set_op(NumericOp.sum) + + def min(self) -> FloatExpr: + return FloatExpr()._new_bind(UnarySetOpBinding(self, RangeSetOp.min)) + + def max(self) -> FloatExpr: + return FloatExpr()._new_bind(UnarySetOpBinding(self, RangeSetOp.max)) + + def intersection(self) -> ArrayEltType: + return self._create_unary_set_op(RangeSetOp.intersection) + + def hull(self) -> ArrayEltType: + return self._create_unary_set_op(RangeSetOp.hull) + + +ArrayBoolLike = Union["ArrayBoolExpr", Sequence[BoolLike]] + + class ArrayBoolExpr(ArrayExpr[BoolExpr, List[bool], ArrayBoolLike]): - _elt_type = BoolExpr + _elt_type = BoolExpr + + def any(self) -> BoolExpr: + return BoolExpr()._new_bind(UnarySetOpBinding(self, BoolOp.op_or)) + + def all(self) -> BoolExpr: + return BoolExpr()._new_bind(UnarySetOpBinding(self, BoolOp.op_and)) - def any(self) -> BoolExpr: - return BoolExpr()._new_bind(UnarySetOpBinding(self, BoolOp.op_or)) + def count(self) -> IntExpr: + return IntExpr()._new_bind(UnarySetOpBinding(self, NumericOp.sum)) - def all(self) -> BoolExpr: - return BoolExpr()._new_bind(UnarySetOpBinding(self, BoolOp.op_and)) - def count(self) -> IntExpr: - return IntExpr()._new_bind(UnarySetOpBinding(self, NumericOp.sum)) +ArrayIntLike = Union["ArrayIntExpr", Sequence[IntLike]] -ArrayIntLike = Union['ArrayIntExpr', Sequence[IntLike]] class ArrayIntExpr(ArrayExpr[IntExpr, List[int], ArrayIntLike]): - _elt_type = IntExpr + _elt_type = IntExpr + + +ArrayFloatLike = Union["ArrayFloatExpr", Sequence[FloatLike]] -ArrayFloatLike = Union['ArrayFloatExpr', Sequence[FloatLike]] class ArrayFloatExpr(ArrayExpr[FloatExpr, List[float], ArrayFloatLike]): - _elt_type = FloatExpr + _elt_type = FloatExpr + + +ArrayRangeLike = Union["ArrayRangeExpr", Sequence[RangeLike]] -ArrayRangeLike = Union['ArrayRangeExpr', Sequence[RangeLike]] class ArrayRangeExpr(ArrayExpr[RangeExpr, List[Range], ArrayRangeLike]): - _elt_type = RangeExpr - - def _create_binary_set_op(self, - lhs: ConstraintExpr, - rhs: ConstraintExpr, - op: NumericOp) -> ArrayRangeExpr: - """Creates a new expression that is the result of a binary operation on inputs, and returns my own type. - Any operand can be of any type (eg, scalar-array, array-array, array-scalar), and it is up to the caller - to ensure this makes sense. No type checking happens here.""" - assert lhs._is_bound() and rhs._is_bound() - return self._new_bind(BinarySetOpBinding(lhs, rhs, op)) - - # TODO support pointwise multiply in the future - def __rtruediv__(self, other: Union[FloatLike, RangeLike]) -> ArrayRangeExpr: - """Broadcast-pointwise invert-and-multiply (division with array as rhs)""" - return self._create_binary_set_op( - self._create_unary_set_op(NumericOp.invert), RangeExpr._to_expr_type(other), NumericOp.mul) - - -ArrayStringLike = Union['ArrayStringExpr', Sequence[StringLike]] + _elt_type = RangeExpr + + def _create_binary_set_op(self, lhs: ConstraintExpr, rhs: ConstraintExpr, op: NumericOp) -> ArrayRangeExpr: + """Creates a new expression that is the result of a binary operation on inputs, and returns my own type. + Any operand can be of any type (eg, scalar-array, array-array, array-scalar), and it is up to the caller + to ensure this makes sense. No type checking happens here.""" + assert lhs._is_bound() and rhs._is_bound() + return self._new_bind(BinarySetOpBinding(lhs, rhs, op)) + + # TODO support pointwise multiply in the future + def __rtruediv__(self, other: Union[FloatLike, RangeLike]) -> ArrayRangeExpr: + """Broadcast-pointwise invert-and-multiply (division with array as rhs)""" + return self._create_binary_set_op( + self._create_unary_set_op(NumericOp.invert), RangeExpr._to_expr_type(other), NumericOp.mul + ) + + +ArrayStringLike = Union["ArrayStringExpr", Sequence[StringLike]] + + class ArrayStringExpr(ArrayExpr[StringExpr, List[str], ArrayStringLike]): - _elt_type = StringExpr + _elt_type = StringExpr diff --git a/edg/core/BaseBackend.py b/edg/core/BaseBackend.py index 182877293..e8b847228 100644 --- a/edg/core/BaseBackend.py +++ b/edg/core/BaseBackend.py @@ -6,9 +6,10 @@ class BaseBackend: - """Abstract base class for a backend, which takes a compiled design, and returns a list - of outputs associated with paths.""" - # to be implemented per backend - @abstractmethod - def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[edgir.LocalPath, str]]: - raise NotImplementedError() + """Abstract base class for a backend, which takes a compiled design, and returns a list + of outputs associated with paths.""" + + # to be implemented per backend + @abstractmethod + def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[edgir.LocalPath, str]]: + raise NotImplementedError() diff --git a/edg/core/BaseRefinementPass.py b/edg/core/BaseRefinementPass.py index 3767e4549..33e9fb891 100644 --- a/edg/core/BaseRefinementPass.py +++ b/edg/core/BaseRefinementPass.py @@ -6,8 +6,10 @@ class BaseRefinementPass: - """Abstract base class for a refinement pass, which takes a compiled design, and returns a list - of additional solved values to be added.""" - # to be implemented per backend - @abstractmethod - def run(self, design: CompiledDesign) -> List[Tuple[edgir.LocalPath, edgir.ValueLit]]: pass + """Abstract base class for a refinement pass, which takes a compiled design, and returns a list + of additional solved values to be added.""" + + # to be implemented per backend + @abstractmethod + def run(self, design: CompiledDesign) -> List[Tuple[edgir.LocalPath, edgir.ValueLit]]: + pass diff --git a/edg/core/Binding.py b/edg/core/Binding.py index 6d1efa9d3..25d2f503f 100644 --- a/edg/core/Binding.py +++ b/edg/core/Binding.py @@ -13,520 +13,528 @@ from .Range import Range if TYPE_CHECKING: - from .Ports import BasePort, Port - from .Array import Vector, BaseVector - from .Blocks import BaseBlock - from .ConstraintExpr import ConstraintExpr, FloatExpr, BoolExpr + from .Ports import BasePort, Port + from .Array import Vector, BaseVector + from .Blocks import BaseBlock + from .ConstraintExpr import ConstraintExpr, FloatExpr, BoolExpr ## These are split mostly to let us have some finer grained types on various ## binding helper functions. + class NumericOp(Enum): - # Additive - add = auto() - negate = auto() - sum = auto() + # Additive + add = auto() + negate = auto() + sum = auto() + + # Multiplicative + mul = auto() + invert = auto() + shrink_mul = auto() - # Multiplicative - mul = auto() - invert = auto() - shrink_mul = auto() class BoolOp(Enum): - op_and = auto() - op_not = auto() - op_or = auto() - op_xor = auto() - implies = auto() + op_and = auto() + op_not = auto() + op_or = auto() + op_xor = auto() + implies = auto() + class EqOp(Enum): - eq = auto() - ne = auto() - all_equal = auto() - all_unique = auto() + eq = auto() + ne = auto() + all_equal = auto() + all_unique = auto() + class OrdOp(Enum): - # Ordering - ge = auto() - gt = auto() - le = auto() - lt = auto() - within = auto() + # Ordering + ge = auto() + gt = auto() + le = auto() + lt = auto() + within = auto() + class RangeSetOp(Enum): - # Range/Set - max = auto() - min = auto() + # Range/Set + max = auto() + min = auto() - # Range - hull = auto() - intersection = auto() - center = auto() - width = auto() + # Range + hull = auto() + intersection = auto() + center = auto() + width = auto() - # Set - equal_any = auto() + # Set + equal_any = auto() class Binding: - """Base class for bindings that indicate what a ConstraintExpr means""" - def is_bound(self) -> bool: - return True + """Base class for bindings that indicate what a ConstraintExpr means""" - @abstractmethod - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: ... + def is_bound(self) -> bool: + return True - @abstractmethod - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: ... + @abstractmethod + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: ... + + @abstractmethod + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: ... + + +ParamParentTypes = Union["Port", "BaseBlock"] # creates a circular module dependency on Core -ParamParentTypes = Union['Port', 'BaseBlock'] # creates a circular module dependency on Core class ParamBinding(Binding): - """Binding that indicates this is a parameter""" - @override - def __repr__(self) -> str: - return f"Param({self.parent})" + """Binding that indicates this is a parameter""" + + @override + def __repr__(self) -> str: + return f"Param({self.parent})" - def __init__(self, parent: ParamParentTypes): - super().__init__() - self.parent = parent + def __init__(self, parent: ParamParentTypes): + super().__init__() + self.parent = parent - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: # element should be returned by the containing ConstraintExpr - return [] + @override + def get_subexprs( + self, + ) -> Iterable[Union[ConstraintExpr, BasePort]]: # element should be returned by the containing ConstraintExpr + return [] - @override - def is_bound(self) -> bool: - # TODO clarify binding logic - from .Ports import Port - if isinstance(self.parent, Port): # ports can be a model - return self.parent._is_bound() - else: - return True + @override + def is_bound(self) -> bool: + # TODO clarify binding logic + from .Ports import Port - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.ref.CopyFrom(ref_map[expr]) + if isinstance(self.parent, Port): # ports can be a model + return self.parent._is_bound() + else: + return True + + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.ref.CopyFrom(ref_map[expr]) class InitParamBinding(ParamBinding): - """Binding that indicates this is a parameter from an __init__ argument. - Can optionally take a value, which is the raw value passed into __init__.""" - def __init__(self, parent: ParamParentTypes, value: Optional[Any] = None): - super().__init__(parent) - self.value = value + """Binding that indicates this is a parameter from an __init__ argument. + Can optionally take a value, which is the raw value passed into __init__.""" + + def __init__(self, parent: ParamParentTypes, value: Optional[Any] = None): + super().__init__(parent) + self.value = value class LiteralBinding(Binding): - """Base class for literal bindings""" - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return [] + """Base class for literal bindings""" + + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return [] - @abstractmethod - def populate_literal_proto(self, pb: edgir.ValueLit) -> None: - raise NotImplementedError + @abstractmethod + def populate_literal_proto(self, pb: edgir.ValueLit) -> None: + raise NotImplementedError - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - self.populate_literal_proto(pb.literal) + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + self.populate_literal_proto(pb.literal) class BoolLiteralBinding(LiteralBinding): - @override - def __repr__(self) -> str: - return f"Lit({self.value})" + @override + def __repr__(self) -> str: + return f"Lit({self.value})" - def __init__(self, value: bool): - super().__init__() - self.value = value + def __init__(self, value: bool): + super().__init__() + self.value = value - @override - def populate_literal_proto(self, pb: edgir.ValueLit) -> None: - pb.boolean.val = self.value + @override + def populate_literal_proto(self, pb: edgir.ValueLit) -> None: + pb.boolean.val = self.value class IntLiteralBinding(LiteralBinding): - @override - def __repr__(self) -> str: - return f"Lit({self.value})" + @override + def __repr__(self) -> str: + return f"Lit({self.value})" - def __init__(self, value: int): - self.value = value + def __init__(self, value: int): + self.value = value - @override - def populate_literal_proto(self, pb: edgir.ValueLit) -> None: - pb.integer.val = self.value + @override + def populate_literal_proto(self, pb: edgir.ValueLit) -> None: + pb.integer.val = self.value class FloatLiteralBinding(LiteralBinding): - @override - def __repr__(self) -> str: - return f"Lit({self.value})" + @override + def __repr__(self) -> str: + return f"Lit({self.value})" - def __init__(self, value: Union[float, int]): - self.value: float = float(value) + def __init__(self, value: Union[float, int]): + self.value: float = float(value) - @override - def populate_literal_proto(self, pb: edgir.ValueLit) -> None: - pb.floating.val = self.value + @override + def populate_literal_proto(self, pb: edgir.ValueLit) -> None: + pb.floating.val = self.value class RangeLiteralBinding(LiteralBinding): - @override - def __repr__(self) -> str: - return f"Lit({self.value})" + @override + def __repr__(self) -> str: + return f"Lit({self.value})" - def __init__(self, value: Range): - self.value = value + def __init__(self, value: Range): + self.value = value - @override - def populate_literal_proto(self, pb: edgir.ValueLit) -> None: - pb.range.minimum.floating.val = self.value.lower - pb.range.maximum.floating.val = self.value.upper + @override + def populate_literal_proto(self, pb: edgir.ValueLit) -> None: + pb.range.minimum.floating.val = self.value.lower + pb.range.maximum.floating.val = self.value.upper class StringLiteralBinding(LiteralBinding): - @override - def __repr__(self) -> str: - return f"Lit({self.value})" + @override + def __repr__(self) -> str: + return f"Lit({self.value})" - def __init__(self, value: str): - super().__init__() - self.value = value + def __init__(self, value: str): + super().__init__() + self.value = value - @override - def populate_literal_proto(self, pb: edgir.ValueLit) -> None: - pb.text.val = self.value + @override + def populate_literal_proto(self, pb: edgir.ValueLit) -> None: + pb.text.val = self.value class ArrayLiteralBinding(LiteralBinding): - @override - def __repr__(self) -> str: - return f"Lit({self.values})" + @override + def __repr__(self) -> str: + return f"Lit({self.values})" - def __init__(self, values: Sequence[LiteralBinding]): - super().__init__() - self.values = values + def __init__(self, values: Sequence[LiteralBinding]): + super().__init__() + self.values = values - @override - def populate_literal_proto(self, pb: edgir.ValueLit) -> None: - pb.array.SetInParent() - for value in self.values: - value.populate_literal_proto(pb.array.elts.add()) + @override + def populate_literal_proto(self, pb: edgir.ValueLit) -> None: + pb.array.SetInParent() + for value in self.values: + value.populate_literal_proto(pb.array.elts.add()) class RangeBuilderBinding(Binding): - @override - def __repr__(self) -> str: - return f"RangeBuilder({self.lower}, {self.upper})" + @override + def __repr__(self) -> str: + return f"RangeBuilder({self.lower}, {self.upper})" - def __init__(self, lower: FloatExpr, upper: FloatExpr): - super().__init__() - self.lower = lower - self.upper = upper + def __init__(self, lower: FloatExpr, upper: FloatExpr): + super().__init__() + self.lower = lower + self.upper = upper - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return chain(self.lower._get_exprs(), self.lower._get_exprs()) + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return chain(self.lower._get_exprs(), self.lower._get_exprs()) - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.binary.op = edgir.BinaryExpr.RANGE - self.lower._populate_expr_proto(pb.binary.lhs, ref_map) - self.upper._populate_expr_proto(pb.binary.rhs, ref_map) + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.binary.op = edgir.BinaryExpr.RANGE + self.lower._populate_expr_proto(pb.binary.lhs, ref_map) + self.upper._populate_expr_proto(pb.binary.rhs, ref_map) class ArrayBinding(Binding): - @override - def __repr__(self) -> str: - return f"Array({self.values})" + @override + def __repr__(self) -> str: + return f"Array({self.values})" - def __init__(self, values: Sequence[ConstraintExpr]): - super().__init__() - self.values = values + def __init__(self, values: Sequence[ConstraintExpr]): + super().__init__() + self.values = values - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return self.values + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return self.values - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.array.SetInParent() - for value in self.values: - value._populate_expr_proto(pb.array.vals.add(), ref_map) + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.array.SetInParent() + for value in self.values: + value._populate_expr_proto(pb.array.vals.add(), ref_map) class UnaryOpBinding(Binding): - @override - def __repr__(self) -> str: - return f"UnaryOp({self.op}, ...)" - - def __init__(self, - src: ConstraintExpr, - op: Union[NumericOp,BoolOp,RangeSetOp]): - self.op_map = { - NumericOp.negate: edgir.UnaryExpr.NEGATE, - NumericOp.invert: edgir.UnaryExpr.INVERT, - BoolOp.op_not: edgir.UnaryExpr.NOT, - RangeSetOp.min: edgir.UnaryExpr.MIN, - RangeSetOp.max: edgir.UnaryExpr.MAX, - RangeSetOp.center: edgir.UnaryExpr.CENTER, - RangeSetOp.width: edgir.UnaryExpr.WIDTH, - } - - super().__init__() - self.src = src - self.op = op - - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return chain(self.src._get_exprs()) - - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.unary.op = self.op_map[self.op] - self.src._populate_expr_proto(pb.unary.val, ref_map) + @override + def __repr__(self) -> str: + return f"UnaryOp({self.op}, ...)" + + def __init__(self, src: ConstraintExpr, op: Union[NumericOp, BoolOp, RangeSetOp]): + self.op_map = { + NumericOp.negate: edgir.UnaryExpr.NEGATE, + NumericOp.invert: edgir.UnaryExpr.INVERT, + BoolOp.op_not: edgir.UnaryExpr.NOT, + RangeSetOp.min: edgir.UnaryExpr.MIN, + RangeSetOp.max: edgir.UnaryExpr.MAX, + RangeSetOp.center: edgir.UnaryExpr.CENTER, + RangeSetOp.width: edgir.UnaryExpr.WIDTH, + } + + super().__init__() + self.src = src + self.op = op + + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return chain(self.src._get_exprs()) + + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.unary.op = self.op_map[self.op] + self.src._populate_expr_proto(pb.unary.val, ref_map) + class UnarySetOpBinding(Binding): - @override - def __repr__(self) -> str: - return f"UnarySetOp({self.op}, ...)" - - def __init__(self, - src: ConstraintExpr, - op: Union[NumericOp,BoolOp,EqOp,RangeSetOp]): - self.op_map = { - NumericOp.negate :edgir.UnarySetExpr.NEGATE, - NumericOp.invert: edgir.UnarySetExpr.INVERT, - NumericOp.sum: edgir.UnarySetExpr.SUM, - BoolOp.op_and: edgir.UnarySetExpr.ALL_TRUE, - BoolOp.op_or: edgir.UnarySetExpr.ANY_TRUE, - EqOp.all_equal: edgir.UnarySetExpr.ALL_EQ, - EqOp.all_unique: edgir.UnarySetExpr.ALL_UNIQUE, - RangeSetOp.min: edgir.UnarySetExpr.MINIMUM, - RangeSetOp.max: edgir.UnarySetExpr.MAXIMUM, - RangeSetOp.intersection: edgir.UnarySetExpr.INTERSECTION, - RangeSetOp.hull: edgir.UnarySetExpr.HULL, - RangeSetOp.equal_any: edgir.UnarySetExpr.SET_EXTRACT, - } - - super().__init__() - self.src = src - self.op = op - - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return chain(self.src._get_exprs()) - - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.unary_set.op = self.op_map[self.op] - self.src._populate_expr_proto(pb.unary_set.vals, ref_map) + @override + def __repr__(self) -> str: + return f"UnarySetOp({self.op}, ...)" + + def __init__(self, src: ConstraintExpr, op: Union[NumericOp, BoolOp, EqOp, RangeSetOp]): + self.op_map = { + NumericOp.negate: edgir.UnarySetExpr.NEGATE, + NumericOp.invert: edgir.UnarySetExpr.INVERT, + NumericOp.sum: edgir.UnarySetExpr.SUM, + BoolOp.op_and: edgir.UnarySetExpr.ALL_TRUE, + BoolOp.op_or: edgir.UnarySetExpr.ANY_TRUE, + EqOp.all_equal: edgir.UnarySetExpr.ALL_EQ, + EqOp.all_unique: edgir.UnarySetExpr.ALL_UNIQUE, + RangeSetOp.min: edgir.UnarySetExpr.MINIMUM, + RangeSetOp.max: edgir.UnarySetExpr.MAXIMUM, + RangeSetOp.intersection: edgir.UnarySetExpr.INTERSECTION, + RangeSetOp.hull: edgir.UnarySetExpr.HULL, + RangeSetOp.equal_any: edgir.UnarySetExpr.SET_EXTRACT, + } + + super().__init__() + self.src = src + self.op = op + + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return chain(self.src._get_exprs()) + + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.unary_set.op = self.op_map[self.op] + self.src._populate_expr_proto(pb.unary_set.vals, ref_map) + class BinaryOpBinding(Binding): - @override - def __repr__(self) -> str: - return f"BinaryOp({self.op}, ...)" - - def __init__(self, - lhs: ConstraintExpr, - rhs: ConstraintExpr, - op: Union[NumericOp, BoolOp, EqOp, OrdOp, RangeSetOp]): - self.op_map = { - # Numeric - NumericOp.add: edgir.BinaryExpr.ADD, - NumericOp.mul: edgir.BinaryExpr.MULT, - NumericOp.shrink_mul: edgir.BinaryExpr.SHRINK_MULT, - # Boolean - BoolOp.op_and: edgir.BinaryExpr.AND, - BoolOp.op_or: edgir.BinaryExpr.OR, - BoolOp.op_xor: edgir.BinaryExpr.XOR, - BoolOp.implies: edgir.BinaryExpr.IMPLIES, - # Equality - EqOp.eq: edgir.BinaryExpr.EQ, - EqOp.ne: edgir.BinaryExpr.NEQ, - # Ordering - OrdOp.lt: edgir.BinaryExpr.LT, - OrdOp.le: edgir.BinaryExpr.LTE, - OrdOp.gt: edgir.BinaryExpr.GT, - OrdOp.ge: edgir.BinaryExpr.GTE, - OrdOp.within: edgir.BinaryExpr.WITHIN, - # Range/Set - RangeSetOp.min: edgir.BinaryExpr.MIN, - RangeSetOp.max: edgir.BinaryExpr.MAX, - # Range - RangeSetOp.intersection: edgir.BinaryExpr.INTERSECTION, - RangeSetOp.hull: edgir.BinaryExpr.HULL, - } - - super().__init__() - self.lhs = lhs - self.rhs = rhs - self.op = op - - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return chain(self.lhs._get_exprs(), self.rhs._get_exprs()) - - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.binary.op = self.op_map[self.op] - self.lhs._populate_expr_proto(pb.binary.lhs, ref_map) - self.rhs._populate_expr_proto(pb.binary.rhs, ref_map) + @override + def __repr__(self) -> str: + return f"BinaryOp({self.op}, ...)" + + def __init__(self, lhs: ConstraintExpr, rhs: ConstraintExpr, op: Union[NumericOp, BoolOp, EqOp, OrdOp, RangeSetOp]): + self.op_map = { + # Numeric + NumericOp.add: edgir.BinaryExpr.ADD, + NumericOp.mul: edgir.BinaryExpr.MULT, + NumericOp.shrink_mul: edgir.BinaryExpr.SHRINK_MULT, + # Boolean + BoolOp.op_and: edgir.BinaryExpr.AND, + BoolOp.op_or: edgir.BinaryExpr.OR, + BoolOp.op_xor: edgir.BinaryExpr.XOR, + BoolOp.implies: edgir.BinaryExpr.IMPLIES, + # Equality + EqOp.eq: edgir.BinaryExpr.EQ, + EqOp.ne: edgir.BinaryExpr.NEQ, + # Ordering + OrdOp.lt: edgir.BinaryExpr.LT, + OrdOp.le: edgir.BinaryExpr.LTE, + OrdOp.gt: edgir.BinaryExpr.GT, + OrdOp.ge: edgir.BinaryExpr.GTE, + OrdOp.within: edgir.BinaryExpr.WITHIN, + # Range/Set + RangeSetOp.min: edgir.BinaryExpr.MIN, + RangeSetOp.max: edgir.BinaryExpr.MAX, + # Range + RangeSetOp.intersection: edgir.BinaryExpr.INTERSECTION, + RangeSetOp.hull: edgir.BinaryExpr.HULL, + } + + super().__init__() + self.lhs = lhs + self.rhs = rhs + self.op = op + + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return chain(self.lhs._get_exprs(), self.rhs._get_exprs()) + + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.binary.op = self.op_map[self.op] + self.lhs._populate_expr_proto(pb.binary.lhs, ref_map) + self.rhs._populate_expr_proto(pb.binary.rhs, ref_map) class BinarySetOpBinding(Binding): - @override - def __repr__(self) -> str: - return f"BinaryOp({self.op}, ...)" - - def __init__(self, - lhset: ConstraintExpr, - rhs: ConstraintExpr, - op: NumericOp): - self.op_map = { - # Numeric - NumericOp.add: edgir.BinarySetExpr.ADD, - NumericOp.mul: edgir.BinarySetExpr.MULT, - } - - super().__init__() - self.lhset = lhset - self.rhs = rhs - self.op = op - - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return chain(self.lhset._get_exprs(), self.rhs._get_exprs()) - - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.binary_set.op = self.op_map[self.op] - self.lhset._populate_expr_proto(pb.binary_set.lhset, ref_map) - self.rhs._populate_expr_proto(pb.binary_set.rhs, ref_map) + @override + def __repr__(self) -> str: + return f"BinaryOp({self.op}, ...)" + + def __init__(self, lhset: ConstraintExpr, rhs: ConstraintExpr, op: NumericOp): + self.op_map = { + # Numeric + NumericOp.add: edgir.BinarySetExpr.ADD, + NumericOp.mul: edgir.BinarySetExpr.MULT, + } + + super().__init__() + self.lhset = lhset + self.rhs = rhs + self.op = op + + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return chain(self.lhset._get_exprs(), self.rhs._get_exprs()) + + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.binary_set.op = self.op_map[self.op] + self.lhset._populate_expr_proto(pb.binary_set.lhset, ref_map) + self.rhs._populate_expr_proto(pb.binary_set.rhs, ref_map) class IfThenElseBinding(Binding): - @override - def __repr__(self) -> str: - return f"IfThenElse(...)" + @override + def __repr__(self) -> str: + return f"IfThenElse(...)" - def __init__(self, cond: BoolExpr, then_val: ConstraintExpr, else_val: ConstraintExpr): - super().__init__() - self.cond = cond - self.then_val = then_val - self.else_val = else_val + def __init__(self, cond: BoolExpr, then_val: ConstraintExpr, else_val: ConstraintExpr): + super().__init__() + self.cond = cond + self.then_val = then_val + self.else_val = else_val - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return chain(self.cond._get_exprs(), self.then_val._get_exprs(), self.else_val._get_exprs()) + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return chain(self.cond._get_exprs(), self.then_val._get_exprs(), self.else_val._get_exprs()) - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - self.cond._populate_expr_proto(pb.ifThenElse.cond, ref_map) - self.then_val._populate_expr_proto(pb.ifThenElse.tru, ref_map) - self.else_val._populate_expr_proto(pb.ifThenElse.fal, ref_map) + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + self.cond._populate_expr_proto(pb.ifThenElse.cond, ref_map) + self.then_val._populate_expr_proto(pb.ifThenElse.tru, ref_map) + self.else_val._populate_expr_proto(pb.ifThenElse.fal, ref_map) class IsConnectedBinding(Binding): - @override - def __repr__(self) -> str: - return f"IsConnected" + @override + def __repr__(self) -> str: + return f"IsConnected" - def __init__(self, src: Port): - super().__init__() - self.src = src + def __init__(self, src: Port): + super().__init__() + self.src = src - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return [self.src] + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return [self.src] - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.ref.CopyFrom(ref_map[self.src]) - pb.ref.steps.add().reserved_param = edgir.IS_CONNECTED + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.ref.CopyFrom(ref_map[self.src]) + pb.ref.steps.add().reserved_param = edgir.IS_CONNECTED class NameBinding(Binding): - @override - def __repr__(self) -> str: - return f"Name" + @override + def __repr__(self) -> str: + return f"Name" - def __init__(self, src: Union[BaseBlock, BasePort]): - super().__init__() - self.src = src + def __init__(self, src: Union[BaseBlock, BasePort]): + super().__init__() + self.src = src - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return [] + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return [] - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.ref.CopyFrom(ref_map[self.src]) - pb.ref.steps.add().reserved_param = edgir.NAME + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.ref.CopyFrom(ref_map[self.src]) + pb.ref.steps.add().reserved_param = edgir.NAME class LengthBinding(Binding): - @override - def __repr__(self) -> str: - return f"Length" + @override + def __repr__(self) -> str: + return f"Length" - def __init__(self, src: BaseVector): - super().__init__() - self.src = src + def __init__(self, src: BaseVector): + super().__init__() + self.src = src - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return [self.src] + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return [self.src] - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.ref.CopyFrom(ref_map[self.src]) - pb.ref.steps.add().reserved_param = edgir.LENGTH + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.ref.CopyFrom(ref_map[self.src]) + pb.ref.steps.add().reserved_param = edgir.LENGTH class AllocatedBinding(Binding): - @override - def __repr__(self) -> str: - return f"Allocated" + @override + def __repr__(self) -> str: + return f"Allocated" - def __init__(self, src: BaseVector): - super().__init__() - self.src = src + def __init__(self, src: BaseVector): + super().__init__() + self.src = src - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return [self.src] + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return [self.src] - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.ref.CopyFrom(ref_map[self.src]) - pb.ref.steps.add().reserved_param = edgir.ALLOCATED + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.ref.CopyFrom(ref_map[self.src]) + pb.ref.steps.add().reserved_param = edgir.ALLOCATED class AssignBinding(Binding): - @staticmethod - def populate_assign_proto(pb: edgir.ValueExpr, target: ConstraintExpr, value: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - # Convenience method to make an assign expr without the rest of this proto infrastructure - pb.assign.dst.CopyFrom(ref_map[target]) - value._populate_expr_proto(pb.assign.src, ref_map) - - @override - def __repr__(self) -> str: - return f"Assign({self.target}, ...)" - - def __init__(self, target: ConstraintExpr, value: ConstraintExpr): - super().__init__() - self.target = target - self.value = value - - @override - def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - return [self.value] - - @override - def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: - pb.assign.dst.CopyFrom(ref_map[self.target]) - self.value._populate_expr_proto(pb.assign.src, ref_map) + @staticmethod + def populate_assign_proto( + pb: edgir.ValueExpr, target: ConstraintExpr, value: ConstraintExpr, ref_map: Refable.RefMapType + ) -> None: + # Convenience method to make an assign expr without the rest of this proto infrastructure + pb.assign.dst.CopyFrom(ref_map[target]) + value._populate_expr_proto(pb.assign.src, ref_map) + + @override + def __repr__(self) -> str: + return f"Assign({self.target}, ...)" + + def __init__(self, target: ConstraintExpr, value: ConstraintExpr): + super().__init__() + self.target = target + self.value = value + + @override + def get_subexprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + return [self.value] + + @override + def populate_expr_proto(self, pb: edgir.ValueExpr, expr: ConstraintExpr, ref_map: Refable.RefMapType) -> None: + pb.assign.dst.CopyFrom(ref_map[self.target]) + self.value._populate_expr_proto(pb.assign.src, ref_map) diff --git a/edg/core/BlockInterfaceMixin.py b/edg/core/BlockInterfaceMixin.py index 4ebb0239b..10a136c03 100644 --- a/edg/core/BlockInterfaceMixin.py +++ b/edg/core/BlockInterfaceMixin.py @@ -6,7 +6,7 @@ from .HdlUserExceptions import BlockDefinitionError from .HierarchyBlock import Block -MixinBaseType = TypeVar('MixinBaseType', covariant=True, bound=Block, default=Block) +MixinBaseType = TypeVar("MixinBaseType", covariant=True, bound=Block, default=Block) @non_library @@ -28,7 +28,9 @@ class BlockInterfaceMixin(Block, Generic[MixinBaseType]): TODO: is this a good decision? TODO: what about cases where it's a bit mixed, e.g. HasI2s also needs to register the self.i2s port? """ - BaseType = TypeVar('BaseType', bound=HasMetadata) + + BaseType = TypeVar("BaseType", bound=HasMetadata) + @classmethod @override def _get_bases_of(cls, base_type: Type[BaseType]) -> Tuple[List[Type[BaseType]], List[Type[BaseType]]]: @@ -36,9 +38,9 @@ def _get_bases_of(cls, base_type: Type[BaseType]) -> Tuple[List[Type[BaseType]], if cls._is_mixin(): # adds the mixin base defined in MixinBaseType to the list of bases mixin_base = cls._get_mixin_base() all_bases = ordered_direct_bases + ordered_indirect_bases - all_bases_has_mixin_base = map(lambda bcls: issubclass(bcls, BlockInterfaceMixin) and - bcls._get_mixin_base() == mixin_base, - all_bases) + all_bases_has_mixin_base = map( + lambda bcls: issubclass(bcls, BlockInterfaceMixin) and bcls._get_mixin_base() == mixin_base, all_bases + ) if any(all_bases_has_mixin_base): # mixin has been inherited, add mixin base to the end ordered_indirect_bases.append(mixin_base) # type: ignore @@ -48,7 +50,7 @@ def _get_bases_of(cls, base_type: Type[BaseType]) -> Tuple[List[Type[BaseType]], return ordered_direct_bases, ordered_indirect_bases @classmethod - def _get_mixin_base(cls) -> Type['BlockInterfaceMixin']: + def _get_mixin_base(cls) -> Type["BlockInterfaceMixin"]: mixin_base: Optional[Type[BlockInterfaceMixin]] = None for bcls in cls.__orig_bases__: # type: ignore if get_origin(bcls) == BlockInterfaceMixin: @@ -64,8 +66,9 @@ def _get_mixin_base(cls) -> Type['BlockInterfaceMixin']: @classmethod def _is_mixin(cls) -> bool: - return BlockInterfaceMixin in cls.__bases__ or\ - all(map(lambda bcls: issubclass(bcls, BlockInterfaceMixin) and bcls._is_mixin(), cls.__bases__)) + return BlockInterfaceMixin in cls.__bases__ or all( + map(lambda bcls: issubclass(bcls, BlockInterfaceMixin) and bcls._is_mixin(), cls.__bases__) + ) def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) @@ -78,6 +81,7 @@ def implementation(self, fn: Callable[[MixinBaseType], None]) -> None: """Wrap implementation definitions (where MixinBaseType is required) in this. This is only called in a concrete class, and ignored when the standalone mixin is instantiated.""" if not self._is_mixin(): - assert isinstance(self, self._get_mixin_base()),\ - f"self {self.__class__} not instance of base {self._get_mixin_base()}" + assert isinstance( + self, self._get_mixin_base() + ), f"self {self.__class__} not instance of base {self._get_mixin_base()}" fn(self) # type: ignore diff --git a/edg/core/Blocks.py b/edg/core/Blocks.py index 8a63b01b1..85fa44576 100644 --- a/edg/core/Blocks.py +++ b/edg/core/Blocks.py @@ -19,538 +19,593 @@ from .Ports import BasePort, Port if TYPE_CHECKING: - from .Link import Link + from .Link import Link class BaseBlockMeta(type): - """Adds a hook to set the post-init elaboration state""" - @override - def __call__(cls, *args: Any, **kwargs: Any) -> Any: - block_context = builder.get_enclosing_block() - obj = super().__call__(*args, **kwargs) - if isinstance(obj, BaseBlock): # ignore block prototypes - obj._block_context = block_context - return obj - - -class Connection(): - """An incremental connection builder, that validates additional ports as they are added so - the stack trace can provide the problematic statement.""" - - @staticmethod - def _baseport_leaf_type(port: BasePort) -> Port: - if isinstance(port, Port): - return port - elif isinstance(port, BaseVector): - return Connection._baseport_leaf_type(port._get_elt_sample()) - else: - raise ValueError(f"Unknown BasePort subtype {port}") - - class ConnectedLink(NamedTuple): # link-mediated connection (including bridged ports and inner links) - link_type: Type[Link] - is_link_array: bool - bridged_connects: List[Tuple[BasePort, edgir.LocalPath]] # external / boundary port <> link port, invalid in links - link_connects: List[Tuple[BasePort, edgir.LocalPath]] # internal block port <> link port - - class Export(NamedTuple): # direct export (1:1, single or vector) - is_array: bool - external_port: BasePort - internal_port: BasePort - - def __init__(self, parent: BaseBlock, flatten: bool) -> None: - self.parent = parent - self.flatten = flatten # vectors are treated as connected to the same link (instead of a link array) - - self.ports: List[BasePort] = [] # all connected ports - - # mutable state for links, initialized when a link is first inferred - self.link_instance: Optional[Link] = None # link instance, if connects have built up to be a link - self.is_link_array = False - self.link_connected_ports: IdentityDict[BasePort, List[BasePort]] = IdentityDict() # link port -> [connected ports] - self.bridged_ports: IdentityDict[BasePort, BasePort] = IdentityDict() # external port -> internal port on bridge - self.available_link_ports_by_type: Dict[Type[Port], List[BasePort]] = {} # sorted by port order - - def _is_export(self) -> Optional[Tuple[BasePort, BasePort]]: # returns (external, internal) ports if export - if len(self.ports) != 2: # two ports, check if it's an export - return None - port0 = self.ports[0] - port1 = self.ports[1] - if port0._type_of() != port1._type_of(): - return None - if port0._block_parent() is self.parent and not port1._block_parent() is self.parent: - return (port0, port1) - elif port1._block_parent() is self.parent and not port0._block_parent() is self.parent: - return (port1, port0) - else: - return None - - def add_ports(self, ports: Iterable[BasePort]) -> None: - from .HierarchyBlock import Block - from .Link import Link + """Adds a hook to set the post-init elaboration state""" + + @override + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + block_context = builder.get_enclosing_block() + obj = super().__call__(*args, **kwargs) + if isinstance(obj, BaseBlock): # ignore block prototypes + obj._block_context = block_context + return obj + - self.ports.extend(ports) - if len(self.ports) < 1: - return # empty connection, doesn't exist - - port0 = self.ports[0] # first port is special, eg to determine link type - if len(self.ports) == 1: - if not (self.flatten and isinstance(port0, BaseVector)): # flatten can result in multiple connections - return # single element connection, can be a naming operation - elif len(self.ports) == 2: - is_export = self._is_export() - if is_export: - (ext_port, int_port) = is_export - initializers = ext_port._get_initializers([]) - if initializers: - raise UnconnectableError(f"Connected boundary port {ext_port._name_from(self.parent, allow_unknown=True)} may not have initializers, " - f"got {', '.join(['.'.join(path) + '=' + str(value) for _, path, value in initializers])}") - return # is an export, not a connection - - # otherwise, is a link-mediated connection - if self.link_instance is None: # if link not yet defined, create using the first port as authoritative - ports_to_connect: Iterable[BasePort] = self.ports # new link, need to allocate all ports - # initialize mutable state - link_type = self._baseport_leaf_type(port0).link_type - link = self.link_instance = link_type() - self.is_link_array = isinstance(port0, BaseVector) and not self.flatten - assert not self.link_connected_ports - assert not self.bridged_ports - assert not self.available_link_ports_by_type - for name, port in link._ports.items(): - self.available_link_ports_by_type.setdefault(type(self._baseport_leaf_type(port)), []).append(port) - else: - link = self.link_instance - ports_to_connect = ports # other ports previously allocated, only allocate new port - - for port in ports_to_connect: - if isinstance(self.parent, Block): # check if bridge is needed - if port._block_parent() is self.parent: - if port._get_initializers([]): - raise UnconnectableError(f"Connected boundary port {port._name_from(self.parent, allow_unknown=True)} may not have initializers") - if not isinstance(port, Port): - raise UnconnectableError(f"Can't generate bridge for non-Port {port._name_from(self.parent, allow_unknown=True)}") - - bridge = port._bridge() - if bridge is None: - raise UnconnectableError(f"No bridge for {port._name_from(self.parent, allow_unknown=True)}") - link_facing_port = self.bridged_ports[port] = bridge.inner_link +class Connection: + """An incremental connection builder, that validates additional ports as they are added so + the stack trace can provide the problematic statement.""" + + @staticmethod + def _baseport_leaf_type(port: BasePort) -> Port: + if isinstance(port, Port): + return port + elif isinstance(port, BaseVector): + return Connection._baseport_leaf_type(port._get_elt_sample()) else: - link_facing_port = port - elif isinstance(self.parent, Link): # links don't bridge, all ports are treated as internal - if port._block_parent() is not self.parent: - raise UnconnectableError(f"Port {port._name_from(self.parent, allow_unknown=True)} not in containing link") - link_facing_port = port - else: - raise ValueError(f"unknown parent {self.parent}") - - if isinstance(link_facing_port, BaseVector): - if not self.is_link_array and not self.flatten: - raise UnconnectableError(f"Can't connect array and non-array ports without flattening") - - # allocate the connection - if self._baseport_leaf_type(link_facing_port).link_type is not type(link): - raise UnconnectableError(f"Can't connect {port._name_from(self.parent, allow_unknown=True)} to link of type {type(link)}") - port_type = type(self._baseport_leaf_type(link_facing_port)) - allocatable_link_ports = self.available_link_ports_by_type.get(port_type, None) - if allocatable_link_ports is None: - raise UnconnectableError(f"No link port for {port._name_from(self.parent, allow_unknown=True)} of type {port_type}") - if not allocatable_link_ports: - raise UnconnectableError(f"No remaining link ports to {port._name_from(self.parent, allow_unknown=True)}") - - allocated_link_port = allocatable_link_ports[0] - if isinstance(allocated_link_port, BaseVector): # array on link side, can connected multiple ports - pass - else: # single port on link side, consumed - assert allocated_link_port not in self.link_connected_ports - allocatable_link_ports.pop(0) - - self.link_connected_ports.setdefault(allocated_link_port, []).append(port) - - def make_connection(self) -> Optional[Union[ConnectedLink, Export]]: - if len(self.ports) < 1: - return None # empty connection, doesn't exist - - port0 = self.ports[0] - if len(self.ports) == 1: - if not (self.flatten and isinstance(port0, BaseVector)): - return None - - is_export = self._is_export() - if is_export: - (ext_port, int_port) = is_export - return Connection.Export(isinstance(ext_port, BaseVector), ext_port, int_port) - - bridged_connects: List[Tuple[BasePort, edgir.LocalPath]] = [] - link_connects: List[Tuple[BasePort, edgir.LocalPath]] = [] - assert self.link_instance is not None - link_ref_map = self.link_instance._create_ref_map() - for link_port, connected_ports in self.link_connected_ports.items(): - link_ref = link_ref_map[link_port] - if isinstance(link_port, BaseVector): - allocate_path = edgir.LocalPath() - allocate_path.CopyFrom(link_ref) - allocate_path.steps.append(edgir.LocalStep(allocate='')) - link_ref = allocate_path - for connected_port in connected_ports: - bridged_port = self.bridged_ports.get(connected_port, None) - if bridged_port is None: # direct connection, no bridge - link_connects.append((connected_port, link_ref)) - else: # bridge - bridged_connects.append((connected_port, link_ref)) - - return Connection.ConnectedLink(type(self.link_instance), self.is_link_array, bridged_connects, link_connects) + raise ValueError(f"Unknown BasePort subtype {port}") + + class ConnectedLink(NamedTuple): # link-mediated connection (including bridged ports and inner links) + link_type: Type[Link] + is_link_array: bool + bridged_connects: List[ + Tuple[BasePort, edgir.LocalPath] + ] # external / boundary port <> link port, invalid in links + link_connects: List[Tuple[BasePort, edgir.LocalPath]] # internal block port <> link port + + class Export(NamedTuple): # direct export (1:1, single or vector) + is_array: bool + external_port: BasePort + internal_port: BasePort + + def __init__(self, parent: BaseBlock, flatten: bool) -> None: + self.parent = parent + self.flatten = flatten # vectors are treated as connected to the same link (instead of a link array) + + self.ports: List[BasePort] = [] # all connected ports + + # mutable state for links, initialized when a link is first inferred + self.link_instance: Optional[Link] = None # link instance, if connects have built up to be a link + self.is_link_array = False + self.link_connected_ports: IdentityDict[BasePort, List[BasePort]] = ( + IdentityDict() + ) # link port -> [connected ports] + self.bridged_ports: IdentityDict[BasePort, BasePort] = ( + IdentityDict() + ) # external port -> internal port on bridge + self.available_link_ports_by_type: Dict[Type[Port], List[BasePort]] = {} # sorted by port order + + def _is_export(self) -> Optional[Tuple[BasePort, BasePort]]: # returns (external, internal) ports if export + if len(self.ports) != 2: # two ports, check if it's an export + return None + port0 = self.ports[0] + port1 = self.ports[1] + if port0._type_of() != port1._type_of(): + return None + if port0._block_parent() is self.parent and not port1._block_parent() is self.parent: + return (port0, port1) + elif port1._block_parent() is self.parent and not port0._block_parent() is self.parent: + return (port1, port0) + else: + return None + + def add_ports(self, ports: Iterable[BasePort]) -> None: + from .HierarchyBlock import Block + from .Link import Link + + self.ports.extend(ports) + if len(self.ports) < 1: + return # empty connection, doesn't exist + + port0 = self.ports[0] # first port is special, eg to determine link type + if len(self.ports) == 1: + if not (self.flatten and isinstance(port0, BaseVector)): # flatten can result in multiple connections + return # single element connection, can be a naming operation + elif len(self.ports) == 2: + is_export = self._is_export() + if is_export: + (ext_port, int_port) = is_export + initializers = ext_port._get_initializers([]) + if initializers: + raise UnconnectableError( + f"Connected boundary port {ext_port._name_from(self.parent, allow_unknown=True)} may not have initializers, " + f"got {', '.join(['.'.join(path) + '=' + str(value) for _, path, value in initializers])}" + ) + return # is an export, not a connection + + # otherwise, is a link-mediated connection + if self.link_instance is None: # if link not yet defined, create using the first port as authoritative + ports_to_connect: Iterable[BasePort] = self.ports # new link, need to allocate all ports + # initialize mutable state + link_type = self._baseport_leaf_type(port0).link_type + link = self.link_instance = link_type() + self.is_link_array = isinstance(port0, BaseVector) and not self.flatten + assert not self.link_connected_ports + assert not self.bridged_ports + assert not self.available_link_ports_by_type + for name, port in link._ports.items(): + self.available_link_ports_by_type.setdefault(type(self._baseport_leaf_type(port)), []).append(port) + else: + link = self.link_instance + ports_to_connect = ports # other ports previously allocated, only allocate new port + + for port in ports_to_connect: + if isinstance(self.parent, Block): # check if bridge is needed + if port._block_parent() is self.parent: + if port._get_initializers([]): + raise UnconnectableError( + f"Connected boundary port {port._name_from(self.parent, allow_unknown=True)} may not have initializers" + ) + if not isinstance(port, Port): + raise UnconnectableError( + f"Can't generate bridge for non-Port {port._name_from(self.parent, allow_unknown=True)}" + ) + + bridge = port._bridge() + if bridge is None: + raise UnconnectableError(f"No bridge for {port._name_from(self.parent, allow_unknown=True)}") + link_facing_port = self.bridged_ports[port] = bridge.inner_link + else: + link_facing_port = port + elif isinstance(self.parent, Link): # links don't bridge, all ports are treated as internal + if port._block_parent() is not self.parent: + raise UnconnectableError( + f"Port {port._name_from(self.parent, allow_unknown=True)} not in containing link" + ) + link_facing_port = port + else: + raise ValueError(f"unknown parent {self.parent}") + + if isinstance(link_facing_port, BaseVector): + if not self.is_link_array and not self.flatten: + raise UnconnectableError(f"Can't connect array and non-array ports without flattening") + + # allocate the connection + if self._baseport_leaf_type(link_facing_port).link_type is not type(link): + raise UnconnectableError( + f"Can't connect {port._name_from(self.parent, allow_unknown=True)} to link of type {type(link)}" + ) + port_type = type(self._baseport_leaf_type(link_facing_port)) + allocatable_link_ports = self.available_link_ports_by_type.get(port_type, None) + if allocatable_link_ports is None: + raise UnconnectableError( + f"No link port for {port._name_from(self.parent, allow_unknown=True)} of type {port_type}" + ) + if not allocatable_link_ports: + raise UnconnectableError( + f"No remaining link ports to {port._name_from(self.parent, allow_unknown=True)}" + ) + + allocated_link_port = allocatable_link_ports[0] + if isinstance(allocated_link_port, BaseVector): # array on link side, can connected multiple ports + pass + else: # single port on link side, consumed + assert allocated_link_port not in self.link_connected_ports + allocatable_link_ports.pop(0) + + self.link_connected_ports.setdefault(allocated_link_port, []).append(port) + + def make_connection(self) -> Optional[Union[ConnectedLink, Export]]: + if len(self.ports) < 1: + return None # empty connection, doesn't exist + + port0 = self.ports[0] + if len(self.ports) == 1: + if not (self.flatten and isinstance(port0, BaseVector)): + return None + + is_export = self._is_export() + if is_export: + (ext_port, int_port) = is_export + return Connection.Export(isinstance(ext_port, BaseVector), ext_port, int_port) + + bridged_connects: List[Tuple[BasePort, edgir.LocalPath]] = [] + link_connects: List[Tuple[BasePort, edgir.LocalPath]] = [] + assert self.link_instance is not None + link_ref_map = self.link_instance._create_ref_map() + for link_port, connected_ports in self.link_connected_ports.items(): + link_ref = link_ref_map[link_port] + if isinstance(link_port, BaseVector): + allocate_path = edgir.LocalPath() + allocate_path.CopyFrom(link_ref) + allocate_path.steps.append(edgir.LocalStep(allocate="")) + link_ref = allocate_path + for connected_port in connected_ports: + bridged_port = self.bridged_ports.get(connected_port, None) + if bridged_port is None: # direct connection, no bridge + link_connects.append((connected_port, link_ref)) + else: # bridge + bridged_connects.append((connected_port, link_ref)) + + return Connection.ConnectedLink(type(self.link_instance), self.is_link_array, bridged_connects, link_connects) class BlockElaborationState(Enum): - pre_init = 1 # not sure if this is needed, doesn't actually get used - init = 2 - contents = 4 - post_contents = 5 - generate = 6 - post_generate = 7 - - -class DescriptionStringElts(): - @abstractmethod - def set_elt_proto(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: - raise NotImplementedError - - -class DescriptionString(): - def __init__(self, *elts: Union[str, DescriptionStringElts]): - self.elts = elts - - def add_to_proto(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: - for elt in self.elts: - if isinstance(elt, DescriptionStringElts): - elt.set_elt_proto(pb, ref_map) - elif isinstance(elt, str): - new_phrase = pb.description.add() - new_phrase.text = elt - - class FormatUnits(DescriptionStringElts): - ref: ConstraintExpr - units: str - def __init__(self, ref: ConstraintExpr, units: str): - self.ref = ref - self.units = units + pre_init = 1 # not sure if this is needed, doesn't actually get used + init = 2 + contents = 4 + post_contents = 5 + generate = 6 + post_generate = 7 - @override + +class DescriptionStringElts: + @abstractmethod def set_elt_proto(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: - new_phrase = pb.description.add() - new_phrase.param.path.CopyFrom(ref_map[self.ref]) - new_phrase.param.unit = self.units + raise NotImplementedError + + +class DescriptionString: + def __init__(self, *elts: Union[str, DescriptionStringElts]): + self.elts = elts + + def add_to_proto(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: + for elt in self.elts: + if isinstance(elt, DescriptionStringElts): + elt.set_elt_proto(pb, ref_map) + elif isinstance(elt, str): + new_phrase = pb.description.add() + new_phrase.text = elt + + class FormatUnits(DescriptionStringElts): + ref: ConstraintExpr + units: str + + def __init__(self, ref: ConstraintExpr, units: str): + self.ref = ref + self.units = units + + @override + def set_elt_proto(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: + new_phrase = pb.description.add() + new_phrase.param.path.CopyFrom(ref_map[self.ref]) + new_phrase.param.unit = self.units AbstractBlockProperty = EltPropertiesBase() + @non_library class BaseBlock(HasMetadata, metaclass=BaseBlockMeta): - """Base block that has ports (IOs), parameters, and constraints between them. - """ - # __init__ should initialize the object with structural information (parameters, fields) - # as well as optionally initialization (parameter defaults) - def __init__(self) -> None: - self._block_context: Optional["Refable"] # set by metaclass, as lexical scope available pre-binding - self._parent: Optional[Union[BaseBlock, Port]] # refined from Optional[Refable] in base LibraryElement - - super().__init__() - - self._elaboration_state = BlockElaborationState.init - - self.description: Optional[DescriptionString] = None # additional string field to be displayed as part of the tooltip for blocks - - self._parameters: SubElementDict[ConstraintExpr] = self.manager.new_dict(ConstraintExpr) - self._param_docs = IdentityDict[ConstraintExpr, str]() - - self._ports: SubElementDict[BasePort] = self.manager.new_dict(BasePort) - self._required_ports = IdentitySet[BasePort]() - self._port_docs = IdentityDict[BasePort, str]() - - self._connects = self.manager.new_dict(Connection, anon_prefix='anon_link') - self._connects_by_port = IdentityDict[BasePort, Connection]() # port -> connection - self._connect_delegateds = IdentityDict[Connection, List[Connection]]() # for net joins, joined connect -> prior connects - self._constraints: SubElementDict[ConstraintExpr] = self.manager.new_dict(ConstraintExpr, anon_prefix='anon_constr') - - self._name = StringExpr()._bind(NameBinding(self)) - - def _all_delegated_connects(self) -> IdentitySet[Connection]: - """Returns all the prior connects that have been superseded by a joined connect""" - return IdentitySet(*itertools.chain(*self._connect_delegateds.values())) - - def _all_connects_of(self, base: Connection) -> IdentitySet[Connection]: - """Returns all connects (including prior / superseded connects) for a joined connect""" - frontier = [base] - delegated_connects = IdentitySet[Connection]() - while frontier: - cur = frontier.pop() - if cur in delegated_connects: - continue - delegated_connects.add(cur) - frontier.extend(self._connect_delegateds.get(cur, [])) - - return delegated_connects - - def name(self) -> StringExpr: - return self._name - - """Overload this method to define the contents of this block""" - def contents(self) -> None: - pass - - @override - def _def_to_proto(self) -> edgir.BlockLikeTypes: - raise NotImplementedError - - def _elaborated_def_to_proto(self) -> edgir.BlockLikeTypes: - prev_element = builder.push_element(self) - assert prev_element is None - try: - assert self._elaboration_state == BlockElaborationState.init - self._elaboration_state = BlockElaborationState.contents - self.contents() - self._elaboration_state = BlockElaborationState.post_contents - finally: - builder.pop_to(prev_element) - - return self._def_to_proto() - - def _populate_def_proto_block_base(self, pb: edgir.BlockLikeTypes) -> None: - """Populates the structural parts of a block proto: parameters, ports, superclasses""" - assert self._elaboration_state == BlockElaborationState.post_contents or \ - self._elaboration_state == BlockElaborationState.post_generate - - self._parameters.finalize() - self._ports.finalize() - - if (self.__class__, AbstractBlockProperty) in self._elt_properties: - assert isinstance(pb, edgir.HierarchyBlock) - pb.is_abstract = True - default_refinement_fn = self._elt_properties[(self.__class__, AbstractBlockProperty)] - if default_refinement_fn is not None: - default_refinement = default_refinement_fn() - assert inspect.isclass(default_refinement), "default refinement must be a type" - assert issubclass(default_refinement, self.__class__), "default refinement must be a subclass" - pb.default_refinement.target.name = default_refinement._static_def_name() - - pb.self_class.target.name = self._get_def_name() - - direct_bases, indirect_bases = self._get_bases_of(BaseBlock) - for cls in direct_bases: - pb.superclasses.add().target.name = cls._static_def_name() - for cls in indirect_bases: - pb.super_superclasses.add().target.name = cls._static_def_name() - - for (name, param) in self._parameters.items(): - assert isinstance(param.binding, ParamBinding) - param._populate_decl_proto(edgir.add_pair(pb.params, name)) - - for (name, port) in self._ports.items(): - port._populate_portlike_proto(edgir.add_pair(pb.ports, name)) - - ref_map = self._create_ref_map() - for (name, port) in self._ports.items(): - if port in self._required_ports: - if isinstance(port, Port): - port.is_connected()._populate_expr_proto( - edgir.add_pair(pb.constraints, f'(reqd){name}'), ref_map) - elif isinstance(port, Vector): - (port.length() > 0)._populate_expr_proto( - edgir.add_pair(pb.constraints, f'(reqd){name}'), ref_map) - else: - raise ValueError(f"unknown non-optional port type {port}") - - self._constraints.finalize() # needed for source locator generation - - self._populate_metadata(pb.meta, self._metadata, ref_map) - - def _populate_def_proto_port_init(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: - for (name, port) in self._ports.items(): - for (param, path, initializer) in port._get_initializers([name]): - AssignBinding.populate_assign_proto(edgir.add_pair(pb.constraints, f"(init){'.'.join(path)}"), - param, param._to_expr_type(initializer), ref_map) - - def _populate_def_proto_param_init(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: - for (name, param) in self._parameters.items(): - if param.initializer is not None: - AssignBinding.populate_assign_proto(edgir.add_pair(pb.constraints, f'(init){name}'), - param, param.initializer, ref_map) - - def _populate_def_proto_block_contents(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: - """Populates the contents of a block proto: constraints""" - assert self._elaboration_state == BlockElaborationState.post_contents or \ - self._elaboration_state == BlockElaborationState.post_generate - - self._constraints.finalize() - - for (name, constraint) in self._constraints.items(): - constraint._populate_expr_proto(edgir.add_pair(pb.constraints, name), ref_map) - - def _populate_def_proto_description(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: - description = self.description - assert(description is None or isinstance(description, DescriptionString)) - if isinstance(description, DescriptionString): - description.add_to_proto(pb, ref_map) - - @override - def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: - super()._build_ref_map(ref_map, prefix) - ref_map[self.name()] = edgir.localpath_concat(prefix, edgir.NAME) - for name, param in self._parameters.items(): - param._build_ref_map(ref_map, edgir.localpath_concat(prefix, name)) - for name, port in self._ports.items(): - port._build_ref_map(ref_map, edgir.localpath_concat(prefix, name)) - - def _bind_in_place(self, parent: Union[BaseBlock, Port]) -> None: - self._parent = parent - - def _check_constraint(self, constraint: ConstraintExpr) -> None: - def check_subexpr(expr: Union[ConstraintExpr, BasePort]) -> None: # TODO rewrite this whole method - if isinstance(expr, ConstraintExpr) and isinstance(expr.binding, ParamBinding): - expr_parent = expr.binding.parent - if isinstance(expr_parent, BasePort): - expr_block_parent = expr_parent._block_parent() - assert expr_block_parent is not None - expr_parent = expr_block_parent - - expr_parent_parent = expr_parent._parent # may be None for top-level - if isinstance(expr_parent_parent, BasePort): # resolve Link parent port to parent block - expr_parent_parent = expr_parent_parent._block_parent() - - if not (expr_parent is self or expr_parent_parent is self): - raise UnreachableParameterError(f"In {self}, constraint references unreachable parameter {expr}. " - "Only own parameters, or immediate contained blocks' parameters can be accessed.") - elif isinstance(expr, BasePort): - block_parent = cast(BaseBlock, expr._block_parent()) - assert block_parent is not None - if not block_parent is self or block_parent._parent is self: - raise UnreachableParameterError(f"In {self}, constraint references unreachable port {expr}. " - "Only own ports, or immediate contained blocks' ports can be accessed.") - - for subexpr in constraint._get_exprs(): - check_subexpr(subexpr) - - def require(self, constraint: BoolLike, name: Optional[str] = None, *, unchecked: bool=False) -> BoolExpr: - constraint_typed = BoolExpr._to_expr_type(constraint) - if not isinstance(name, (str, type(None))): - raise EdgTypeError(f"require(...) name", name, (str, None)) - - if not unchecked: # before we have const prop need to manually set nested params - self._check_constraint(constraint_typed) - - self._constraints.register(constraint_typed) - - if name: # TODO unify naming API with everything else? - self.manager.add_element(name, constraint_typed) - - return constraint_typed - - ConstrType = TypeVar('ConstrType') - def assign(self, target: ConstraintExpr[ConstrType, Any], - value: Union[ConstraintExpr[ConstrType, Any], ConstrType], - name: Optional[str] = None) -> AssignExpr: - if not isinstance(target, ConstraintExpr): - raise EdgTypeError(f"assign(...) target", target, ConstraintExpr) - if not isinstance(name, (str, type(None))): - raise EdgTypeError(f"assign(...) name", name, (str, None)) - - self._check_constraint(target) - expr_value = target._to_expr_type(value) - self._check_constraint(expr_value) - - constraint = AssignExpr()._bind(AssignBinding(target, expr_value)) - self._constraints.register(constraint) - - if name: # TODO unify naming API with everything else? - self.manager.add_element(name, constraint) - - return constraint - - T = TypeVar('T', bound=BasePort) - def Port(self, tpe: T, *, optional: bool = False, doc: Optional[str] = None) -> T: - """Registers a port for this Block""" - if self._elaboration_state != BlockElaborationState.init: - raise BlockDefinitionError(type(self), "can't call Port(...) outside __init__", - "call Port(...) inside __init__ only, and remember to call super().__init__()") - if not isinstance(tpe, BasePort): - raise EdgTypeError(f"param to Port(...)", tpe, BasePort) - - elt = tpe._bind(self) - self._ports.register(elt) - - if not optional: - self._required_ports.add(elt) - - if doc is not None: - self._port_docs[elt] = doc - - return elt - - ConstraintType = TypeVar('ConstraintType', bound=ConstraintExpr) - def Parameter(self, tpe: ConstraintType, *, doc: Optional[str] = None) -> ConstraintType: - """Registers a parameter for this Block""" - if self._elaboration_state != BlockElaborationState.init: - raise BlockDefinitionError(type(self), "can't call Parameter(...) outside __init__", - "call Parameter(...) inside __init__ only, and remember to call super().__init__()") - if not isinstance(tpe, ConstraintExpr): - raise EdgTypeError(f"param to Parameter(...)", tpe, ConstraintExpr) - - elt = tpe._bind(ParamBinding(self)) - self._parameters.register(elt) - if elt.initializer is not None: - self._check_constraint(elt.initializer) - - if doc is not None: - self._param_docs[elt] = doc - - return elt - - def connect(self, *connects: Union[BasePort, Connection], flatten: bool=False) -> Connection: - for connect in connects: - if not isinstance(connect, (BasePort, Connection)): - raise EdgTypeError(f"param to connect(...)", connect, (BasePort, Connection)) - - connects_ports = [connect for connect in connects if isinstance(connect, BasePort)] - connects_connects = [connect for connect in connects if isinstance(connect, Connection)] - - connects_ports_new = [] - connects_ports_connects = [] - for port in connects_ports: - port_connect = self._connects_by_port.get(port, None) - if port_connect is None: - connects_ports_new.append(port) - else: - connects_ports_connects.append(port_connect) - - existing_connects = connects_connects + connects_ports_connects - if not existing_connects: - connect = Connection(self, flatten) - self._connects.register(connect) - else: # append to (potentially multiple) existing connect - connect = existing_connects[0] # first one is authoritative - connect_flattens = set([connect.flatten for connect in existing_connects]) - assert len(connect_flattens) == 1, "all existing connects must have same flatten" - assert connect_flattens.pop() == flatten, "flatten must be equivalent to existing connect" - - for merge_connect in reversed(existing_connects[1:]): # preserve order - connect.add_ports(merge_connect.ports) - for port in merge_connect.ports: - self._connects_by_port.update(port, connect) - self._connect_delegateds.setdefault(connect, []).append(merge_connect) - - for port in connects_ports_new: - if port._block_parent() is not self: - enclosing_block = port._block_parent() - assert enclosing_block is not None - if enclosing_block._parent is not self: - raise UnconnectableError("Inaccessible port: must be own port or inner block port") - if not(port._parent is enclosing_block or \ - (isinstance(port._parent, Vector) and port._parent._parent is enclosing_block)): - raise UnconnectableError("Inaccessible port: cannot connect inner ports on inner blocks") - self._connects_by_port[port] = connect - connect.add_ports(connects_ports_new) - - return connect - - def ref_to_proto(self) -> str: - return self.__class__.__module__ + "." + self.__class__.__name__ + """Base block that has ports (IOs), parameters, and constraints between them.""" + + # __init__ should initialize the object with structural information (parameters, fields) + # as well as optionally initialization (parameter defaults) + def __init__(self) -> None: + self._block_context: Optional["Refable"] # set by metaclass, as lexical scope available pre-binding + self._parent: Optional[Union[BaseBlock, Port]] # refined from Optional[Refable] in base LibraryElement + + super().__init__() + + self._elaboration_state = BlockElaborationState.init + + self.description: Optional[DescriptionString] = ( + None # additional string field to be displayed as part of the tooltip for blocks + ) + + self._parameters: SubElementDict[ConstraintExpr] = self.manager.new_dict(ConstraintExpr) + self._param_docs = IdentityDict[ConstraintExpr, str]() + + self._ports: SubElementDict[BasePort] = self.manager.new_dict(BasePort) + self._required_ports = IdentitySet[BasePort]() + self._port_docs = IdentityDict[BasePort, str]() + + self._connects = self.manager.new_dict(Connection, anon_prefix="anon_link") + self._connects_by_port = IdentityDict[BasePort, Connection]() # port -> connection + self._connect_delegateds = IdentityDict[ + Connection, List[Connection] + ]() # for net joins, joined connect -> prior connects + self._constraints: SubElementDict[ConstraintExpr] = self.manager.new_dict( + ConstraintExpr, anon_prefix="anon_constr" + ) + + self._name = StringExpr()._bind(NameBinding(self)) + + def _all_delegated_connects(self) -> IdentitySet[Connection]: + """Returns all the prior connects that have been superseded by a joined connect""" + return IdentitySet(*itertools.chain(*self._connect_delegateds.values())) + + def _all_connects_of(self, base: Connection) -> IdentitySet[Connection]: + """Returns all connects (including prior / superseded connects) for a joined connect""" + frontier = [base] + delegated_connects = IdentitySet[Connection]() + while frontier: + cur = frontier.pop() + if cur in delegated_connects: + continue + delegated_connects.add(cur) + frontier.extend(self._connect_delegateds.get(cur, [])) + + return delegated_connects + + def name(self) -> StringExpr: + return self._name + + """Overload this method to define the contents of this block""" + + def contents(self) -> None: + pass + + @override + def _def_to_proto(self) -> edgir.BlockLikeTypes: + raise NotImplementedError + + def _elaborated_def_to_proto(self) -> edgir.BlockLikeTypes: + prev_element = builder.push_element(self) + assert prev_element is None + try: + assert self._elaboration_state == BlockElaborationState.init + self._elaboration_state = BlockElaborationState.contents + self.contents() + self._elaboration_state = BlockElaborationState.post_contents + finally: + builder.pop_to(prev_element) + + return self._def_to_proto() + + def _populate_def_proto_block_base(self, pb: edgir.BlockLikeTypes) -> None: + """Populates the structural parts of a block proto: parameters, ports, superclasses""" + assert ( + self._elaboration_state == BlockElaborationState.post_contents + or self._elaboration_state == BlockElaborationState.post_generate + ) + + self._parameters.finalize() + self._ports.finalize() + + if (self.__class__, AbstractBlockProperty) in self._elt_properties: + assert isinstance(pb, edgir.HierarchyBlock) + pb.is_abstract = True + default_refinement_fn = self._elt_properties[(self.__class__, AbstractBlockProperty)] + if default_refinement_fn is not None: + default_refinement = default_refinement_fn() + assert inspect.isclass(default_refinement), "default refinement must be a type" + assert issubclass(default_refinement, self.__class__), "default refinement must be a subclass" + pb.default_refinement.target.name = default_refinement._static_def_name() + + pb.self_class.target.name = self._get_def_name() + + direct_bases, indirect_bases = self._get_bases_of(BaseBlock) + for cls in direct_bases: + pb.superclasses.add().target.name = cls._static_def_name() + for cls in indirect_bases: + pb.super_superclasses.add().target.name = cls._static_def_name() + + for name, param in self._parameters.items(): + assert isinstance(param.binding, ParamBinding) + param._populate_decl_proto(edgir.add_pair(pb.params, name)) + + for name, port in self._ports.items(): + port._populate_portlike_proto(edgir.add_pair(pb.ports, name)) + + ref_map = self._create_ref_map() + for name, port in self._ports.items(): + if port in self._required_ports: + if isinstance(port, Port): + port.is_connected()._populate_expr_proto(edgir.add_pair(pb.constraints, f"(reqd){name}"), ref_map) + elif isinstance(port, Vector): + (port.length() > 0)._populate_expr_proto(edgir.add_pair(pb.constraints, f"(reqd){name}"), ref_map) + else: + raise ValueError(f"unknown non-optional port type {port}") + + self._constraints.finalize() # needed for source locator generation + + self._populate_metadata(pb.meta, self._metadata, ref_map) + + def _populate_def_proto_port_init(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: + for name, port in self._ports.items(): + for param, path, initializer in port._get_initializers([name]): + AssignBinding.populate_assign_proto( + edgir.add_pair(pb.constraints, f"(init){'.'.join(path)}"), + param, + param._to_expr_type(initializer), + ref_map, + ) + + def _populate_def_proto_param_init(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: + for name, param in self._parameters.items(): + if param.initializer is not None: + AssignBinding.populate_assign_proto( + edgir.add_pair(pb.constraints, f"(init){name}"), param, param.initializer, ref_map + ) + + def _populate_def_proto_block_contents(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: + """Populates the contents of a block proto: constraints""" + assert ( + self._elaboration_state == BlockElaborationState.post_contents + or self._elaboration_state == BlockElaborationState.post_generate + ) + + self._constraints.finalize() + + for name, constraint in self._constraints.items(): + constraint._populate_expr_proto(edgir.add_pair(pb.constraints, name), ref_map) + + def _populate_def_proto_description(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: + description = self.description + assert description is None or isinstance(description, DescriptionString) + if isinstance(description, DescriptionString): + description.add_to_proto(pb, ref_map) + + @override + def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: + super()._build_ref_map(ref_map, prefix) + ref_map[self.name()] = edgir.localpath_concat(prefix, edgir.NAME) + for name, param in self._parameters.items(): + param._build_ref_map(ref_map, edgir.localpath_concat(prefix, name)) + for name, port in self._ports.items(): + port._build_ref_map(ref_map, edgir.localpath_concat(prefix, name)) + + def _bind_in_place(self, parent: Union[BaseBlock, Port]) -> None: + self._parent = parent + + def _check_constraint(self, constraint: ConstraintExpr) -> None: + def check_subexpr(expr: Union[ConstraintExpr, BasePort]) -> None: # TODO rewrite this whole method + if isinstance(expr, ConstraintExpr) and isinstance(expr.binding, ParamBinding): + expr_parent = expr.binding.parent + if isinstance(expr_parent, BasePort): + expr_block_parent = expr_parent._block_parent() + assert expr_block_parent is not None + expr_parent = expr_block_parent + + expr_parent_parent = expr_parent._parent # may be None for top-level + if isinstance(expr_parent_parent, BasePort): # resolve Link parent port to parent block + expr_parent_parent = expr_parent_parent._block_parent() + + if not (expr_parent is self or expr_parent_parent is self): + raise UnreachableParameterError( + f"In {self}, constraint references unreachable parameter {expr}. " + "Only own parameters, or immediate contained blocks' parameters can be accessed." + ) + elif isinstance(expr, BasePort): + block_parent = cast(BaseBlock, expr._block_parent()) + assert block_parent is not None + if not block_parent is self or block_parent._parent is self: + raise UnreachableParameterError( + f"In {self}, constraint references unreachable port {expr}. " + "Only own ports, or immediate contained blocks' ports can be accessed." + ) + + for subexpr in constraint._get_exprs(): + check_subexpr(subexpr) + + def require(self, constraint: BoolLike, name: Optional[str] = None, *, unchecked: bool = False) -> BoolExpr: + constraint_typed = BoolExpr._to_expr_type(constraint) + if not isinstance(name, (str, type(None))): + raise EdgTypeError(f"require(...) name", name, (str, None)) + + if not unchecked: # before we have const prop need to manually set nested params + self._check_constraint(constraint_typed) + + self._constraints.register(constraint_typed) + + if name: # TODO unify naming API with everything else? + self.manager.add_element(name, constraint_typed) + + return constraint_typed + + ConstrType = TypeVar("ConstrType") + + def assign( + self, + target: ConstraintExpr[ConstrType, Any], + value: Union[ConstraintExpr[ConstrType, Any], ConstrType], + name: Optional[str] = None, + ) -> AssignExpr: + if not isinstance(target, ConstraintExpr): + raise EdgTypeError(f"assign(...) target", target, ConstraintExpr) + if not isinstance(name, (str, type(None))): + raise EdgTypeError(f"assign(...) name", name, (str, None)) + + self._check_constraint(target) + expr_value = target._to_expr_type(value) + self._check_constraint(expr_value) + + constraint = AssignExpr()._bind(AssignBinding(target, expr_value)) + self._constraints.register(constraint) + + if name: # TODO unify naming API with everything else? + self.manager.add_element(name, constraint) + + return constraint + + T = TypeVar("T", bound=BasePort) + + def Port(self, tpe: T, *, optional: bool = False, doc: Optional[str] = None) -> T: + """Registers a port for this Block""" + if self._elaboration_state != BlockElaborationState.init: + raise BlockDefinitionError( + type(self), + "can't call Port(...) outside __init__", + "call Port(...) inside __init__ only, and remember to call super().__init__()", + ) + if not isinstance(tpe, BasePort): + raise EdgTypeError(f"param to Port(...)", tpe, BasePort) + + elt = tpe._bind(self) + self._ports.register(elt) + + if not optional: + self._required_ports.add(elt) + + if doc is not None: + self._port_docs[elt] = doc + + return elt + + ConstraintType = TypeVar("ConstraintType", bound=ConstraintExpr) + + def Parameter(self, tpe: ConstraintType, *, doc: Optional[str] = None) -> ConstraintType: + """Registers a parameter for this Block""" + if self._elaboration_state != BlockElaborationState.init: + raise BlockDefinitionError( + type(self), + "can't call Parameter(...) outside __init__", + "call Parameter(...) inside __init__ only, and remember to call super().__init__()", + ) + if not isinstance(tpe, ConstraintExpr): + raise EdgTypeError(f"param to Parameter(...)", tpe, ConstraintExpr) + + elt = tpe._bind(ParamBinding(self)) + self._parameters.register(elt) + if elt.initializer is not None: + self._check_constraint(elt.initializer) + + if doc is not None: + self._param_docs[elt] = doc + + return elt + + def connect(self, *connects: Union[BasePort, Connection], flatten: bool = False) -> Connection: + for connect in connects: + if not isinstance(connect, (BasePort, Connection)): + raise EdgTypeError(f"param to connect(...)", connect, (BasePort, Connection)) + + connects_ports = [connect for connect in connects if isinstance(connect, BasePort)] + connects_connects = [connect for connect in connects if isinstance(connect, Connection)] + + connects_ports_new = [] + connects_ports_connects = [] + for port in connects_ports: + port_connect = self._connects_by_port.get(port, None) + if port_connect is None: + connects_ports_new.append(port) + else: + connects_ports_connects.append(port_connect) + + existing_connects = connects_connects + connects_ports_connects + if not existing_connects: + connect = Connection(self, flatten) + self._connects.register(connect) + else: # append to (potentially multiple) existing connect + connect = existing_connects[0] # first one is authoritative + connect_flattens = set([connect.flatten for connect in existing_connects]) + assert len(connect_flattens) == 1, "all existing connects must have same flatten" + assert connect_flattens.pop() == flatten, "flatten must be equivalent to existing connect" + + for merge_connect in reversed(existing_connects[1:]): # preserve order + connect.add_ports(merge_connect.ports) + for port in merge_connect.ports: + self._connects_by_port.update(port, connect) + self._connect_delegateds.setdefault(connect, []).append(merge_connect) + + for port in connects_ports_new: + if port._block_parent() is not self: + enclosing_block = port._block_parent() + assert enclosing_block is not None + if enclosing_block._parent is not self: + raise UnconnectableError("Inaccessible port: must be own port or inner block port") + if not ( + port._parent is enclosing_block + or (isinstance(port._parent, Vector) and port._parent._parent is enclosing_block) + ): + raise UnconnectableError("Inaccessible port: cannot connect inner ports on inner blocks") + self._connects_by_port[port] = connect + connect.add_ports(connects_ports_new) + + return connect + + def ref_to_proto(self) -> str: + return self.__class__.__module__ + "." + self.__class__.__name__ diff --git a/edg/core/BufferSerializer.py b/edg/core/BufferSerializer.py index bf6c9eb17..3c7463b07 100644 --- a/edg/core/BufferSerializer.py +++ b/edg/core/BufferSerializer.py @@ -4,74 +4,78 @@ import struct -MessageType = TypeVar('MessageType', bound=protobuf.message.Message) -kHeaderMagicByte = b'\xfe' +MessageType = TypeVar("MessageType", bound=protobuf.message.Message) +kHeaderMagicByte = b"\xfe" class BufferSerializer(Generic[MessageType]): - """ - Serializes a protobuf message and writes it into the byte buffer, - using a delimited framing consistent with the Java implementation. - """ - def __init__(self, buffer: IO[bytes]): - self.buffer = buffer - - def write(self, message: MessageType) -> None: - from google.protobuf.internal.encoder import _VarintBytes # type: ignore - # from https://cwiki.apache.org/confluence/display/GEODE/Delimiting+Protobuf+Messages - serialized = message.SerializeToString() - self.buffer.write(kHeaderMagicByte) - self.buffer.write(_VarintBytes(len(serialized))) - self.buffer.write(serialized) - self.buffer.flush() + """ + Serializes a protobuf message and writes it into the byte buffer, + using a delimited framing consistent with the Java implementation. + """ + + def __init__(self, buffer: IO[bytes]): + self.buffer = buffer + + def write(self, message: MessageType) -> None: + from google.protobuf.internal.encoder import _VarintBytes # type: ignore + + # from https://cwiki.apache.org/confluence/display/GEODE/Delimiting+Protobuf+Messages + serialized = message.SerializeToString() + self.buffer.write(kHeaderMagicByte) + self.buffer.write(_VarintBytes(len(serialized))) + self.buffer.write(serialized) + self.buffer.flush() class BufferDeserializer(Generic[MessageType]): - """ - Deserializes protobuf-serialized messages from a byte buffer and returns it one message at a time, - using a delimited framing consistent with the Java implementation. - """ - def __init__(self, message_type: Type[MessageType], buffer: IO[bytes]): - self.message_type = message_type - self.buffer = buffer - self.stdout_buffer = b'' - - # Returns the next message from the buffer - def read(self) -> Optional[MessageType]: - while True: - new = self.buffer.read(1) - if not new: - return None - elif new == kHeaderMagicByte: - break - else: - self.stdout_buffer += new - - from google.protobuf.internal.decoder import _DecodeVarint32 # type: ignore - # from https://cwiki.apache.org/confluence/display/GEODE/Delimiting+Protobuf+Messages - current = b'' - while len(current) == 0 or current[-1] & 0x80 == 0x80: - new = self.buffer.read(1) - if not new: - return None - current = current + new - size, new_pos = _DecodeVarint32(current, 0) - assert new_pos == len(current) - - current = b'' - while len(current) < size: - new = self.buffer.read(size - len(current)) - if not new: - return None - current = current + new - assert(len(current) == size) - - message = self.message_type() - message.ParseFromString(current) - - return message - - def read_stdout(self) -> bytes: - old_buffer = self.stdout_buffer - self.stdout_buffer = b'' - return old_buffer + """ + Deserializes protobuf-serialized messages from a byte buffer and returns it one message at a time, + using a delimited framing consistent with the Java implementation. + """ + + def __init__(self, message_type: Type[MessageType], buffer: IO[bytes]): + self.message_type = message_type + self.buffer = buffer + self.stdout_buffer = b"" + + # Returns the next message from the buffer + def read(self) -> Optional[MessageType]: + while True: + new = self.buffer.read(1) + if not new: + return None + elif new == kHeaderMagicByte: + break + else: + self.stdout_buffer += new + + from google.protobuf.internal.decoder import _DecodeVarint32 # type: ignore + + # from https://cwiki.apache.org/confluence/display/GEODE/Delimiting+Protobuf+Messages + current = b"" + while len(current) == 0 or current[-1] & 0x80 == 0x80: + new = self.buffer.read(1) + if not new: + return None + current = current + new + size, new_pos = _DecodeVarint32(current, 0) + assert new_pos == len(current) + + current = b"" + while len(current) < size: + new = self.buffer.read(size - len(current)) + if not new: + return None + current = current + new + assert len(current) == size + + message = self.message_type() + message.ParseFromString(current) + + return message + + def read_stdout(self) -> bytes: + old_buffer = self.stdout_buffer + self.stdout_buffer = b"" + return old_buffer diff --git a/edg/core/Builder.py b/edg/core/Builder.py index f2f9e70eb..78c10dd37 100644 --- a/edg/core/Builder.py +++ b/edg/core/Builder.py @@ -7,65 +7,78 @@ from .. import edgir if TYPE_CHECKING: - from .Blocks import BaseBlock - from .Link import Link - from .HierarchyBlock import Block + from .Blocks import BaseBlock + from .Link import Link + from .HierarchyBlock import Block class Builder: - def __init__(self) -> None: - self.stack: List[BaseBlock] = [] - - def push_element(self, elt: BaseBlock) -> Optional[BaseBlock]: - """Pushes a new element onto the context stack, returning the previous top element. - Ignores if the element is already on top of the stack.""" - prev_elt = self.get_enclosing_block() - if not self.stack or self.stack[-1] is not elt: # prevent double-pushing - self.stack.append(elt) - return prev_elt - - def pop_to(self, prev: Optional[BaseBlock]) -> None: - """Pops at most one element from stack, expecting prev to be at the top of the stack. - The pattern should be one pop for one push, and allowing that duplicate pushes are ignored.""" - if (prev is None and not self.stack) or (prev is not None and self.stack[-1] is prev): - return - - self.stack.pop() - assert (prev is None and not self.stack) or (prev is not None and self.stack[-1] is prev) - - def get_enclosing_block(self) -> Optional[BaseBlock]: - if not self.stack: - return None - else: - return self.stack[-1] - - @deprecated("use get_enclosing_block() instead, context frames can only be blocks now") - def get_curr_context(self) -> Optional[BaseBlock]: - return self.get_enclosing_block() - - @overload - def elaborate_toplevel(self, block: Block, *, - is_generator: bool = False, - generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]] = []) -> edgir.HierarchyBlock: ... - @overload - def elaborate_toplevel(self, block: Link, *, - is_generator: bool = False, - generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]] = []) -> edgir.Link: ... - - def elaborate_toplevel(self, block: BaseBlock, *, - is_generator: bool = False, - generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]] = []) -> edgir.BlockLikeTypes: - try: - if is_generator: # TODO this is kind of nasty =( - from .Generator import GeneratorBlock - assert isinstance(block, GeneratorBlock) - elaborated: edgir.BlockLikeTypes = block._generated_def_to_proto(generate_values) - else: # TODO check is a GeneratorBlock w/o circular imports? - elaborated = block._elaborated_def_to_proto() - - return elaborated - except Exception as e: - raise Exception(f"While elaborating {block.__class__}") from e + def __init__(self) -> None: + self.stack: List[BaseBlock] = [] + + def push_element(self, elt: BaseBlock) -> Optional[BaseBlock]: + """Pushes a new element onto the context stack, returning the previous top element. + Ignores if the element is already on top of the stack.""" + prev_elt = self.get_enclosing_block() + if not self.stack or self.stack[-1] is not elt: # prevent double-pushing + self.stack.append(elt) + return prev_elt + + def pop_to(self, prev: Optional[BaseBlock]) -> None: + """Pops at most one element from stack, expecting prev to be at the top of the stack. + The pattern should be one pop for one push, and allowing that duplicate pushes are ignored.""" + if (prev is None and not self.stack) or (prev is not None and self.stack[-1] is prev): + return + + self.stack.pop() + assert (prev is None and not self.stack) or (prev is not None and self.stack[-1] is prev) + + def get_enclosing_block(self) -> Optional[BaseBlock]: + if not self.stack: + return None + else: + return self.stack[-1] + + @deprecated("use get_enclosing_block() instead, context frames can only be blocks now") + def get_curr_context(self) -> Optional[BaseBlock]: + return self.get_enclosing_block() + + @overload + def elaborate_toplevel( + self, + block: Block, + *, + is_generator: bool = False, + generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]] = [], + ) -> edgir.HierarchyBlock: ... + @overload + def elaborate_toplevel( + self, + block: Link, + *, + is_generator: bool = False, + generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]] = [], + ) -> edgir.Link: ... + + def elaborate_toplevel( + self, + block: BaseBlock, + *, + is_generator: bool = False, + generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]] = [], + ) -> edgir.BlockLikeTypes: + try: + if is_generator: # TODO this is kind of nasty =( + from .Generator import GeneratorBlock + + assert isinstance(block, GeneratorBlock) + elaborated: edgir.BlockLikeTypes = block._generated_def_to_proto(generate_values) + else: # TODO check is a GeneratorBlock w/o circular imports? + elaborated = block._elaborated_def_to_proto() + + return elaborated + except Exception as e: + raise Exception(f"While elaborating {block.__class__}") from e builder = Builder() diff --git a/edg/core/Categories.py b/edg/core/Categories.py index 8d91cfa05..13046deae 100644 --- a/edg/core/Categories.py +++ b/edg/core/Categories.py @@ -4,6 +4,7 @@ @abstract_block class InternalBlock(Block): - """Internal blocks that are primarily an implementation detail or automatically generated by the system, - and not meant for system-level users to instantiate.""" - pass + """Internal blocks that are primarily an implementation detail or automatically generated by the system, + and not meant for system-level users to instantiate.""" + + pass diff --git a/edg/core/ConstraintExpr.py b/edg/core/ConstraintExpr.py index 52f033b09..fcd9c0f1f 100644 --- a/edg/core/ConstraintExpr.py +++ b/edg/core/ConstraintExpr.py @@ -7,9 +7,19 @@ from itertools import chain from typing_extensions import TypeVar, override -from .Binding import Binding, ParamBinding, BoolLiteralBinding, IntLiteralBinding, \ - FloatLiteralBinding, RangeLiteralBinding, StringLiteralBinding, RangeBuilderBinding, \ - UnaryOpBinding, BinaryOpBinding, IfThenElseBinding +from .Binding import ( + Binding, + ParamBinding, + BoolLiteralBinding, + IntLiteralBinding, + FloatLiteralBinding, + RangeLiteralBinding, + StringLiteralBinding, + RangeBuilderBinding, + UnaryOpBinding, + BinaryOpBinding, + IfThenElseBinding, +) from .Binding import NumericOp, BoolOp, EqOp, OrdOp, RangeSetOp from .Builder import builder from .Core import Refable @@ -17,619 +27,628 @@ from .. import edgir if TYPE_CHECKING: - from .Ports import BasePort - from .Blocks import BaseBlock + from .Ports import BasePort + from .Blocks import BaseBlock + + +SelfType = TypeVar("SelfType", bound="ConstraintExpr") +WrappedType = TypeVar("WrappedType", covariant=True, default=Any) +CastableType = TypeVar("CastableType", contravariant=True, default=Any) -SelfType = TypeVar('SelfType', bound='ConstraintExpr') -WrappedType = TypeVar('WrappedType', covariant=True, default=Any) -CastableType = TypeVar('CastableType', contravariant=True, default=Any) class ConstraintExpr(Refable, Generic[WrappedType, CastableType]): - """Base class for constraint expressions. Basically a container for operations. - Actual meaning is held in the Binding. - """ - _CASTABLE_TYPES: Tuple[Type[CastableType], ...] # for use in ininstance(), excluding self-cls - - @override - def __repr__(self) -> str: - if self.binding is not None and self.initializer is not None: - return f"{super().__repr__()}({self.binding})={self.initializer}" - if self.initializer is not None: - return f"{super().__repr__()}={self.initializer}" - if self.binding is not None: - return f"{super().__repr__()}({self.binding})" - else: - return f"{super().__repr__()}" - - @classmethod - @abstractmethod - def _to_expr_type(cls: Type[SelfType], input: CastableType) -> SelfType: - """Casts the input from an equivalent-type to the self-type.""" - raise NotImplementedError - - @classmethod - @abstractmethod - def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: - """Returns the protobuf for the definition of this parameter. Must have ParamBinding / ParamVariableBinding""" - raise NotImplementedError - - @classmethod - @abstractmethod - def _from_lit(cls, pb: edgir.ValueLit) -> WrappedType: - """Parses the literal / wrapped type from a ValueLit proto.""" - raise NotImplementedError - - def __init__(self: SelfType, initializer: Optional[Union[SelfType, WrappedType]] = None): - self.binding: Optional[Binding] = None - if isinstance(initializer, type(self)) and not initializer._is_bound(): # model passed in - self.initializer: Optional[SelfType] = initializer.initializer - elif initializer is None: - self.initializer = None - else: - self.initializer = self._to_expr_type(initializer) - self._context: Optional[BaseBlock] = builder.get_enclosing_block() - - def _get_exprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: - assert self.binding is not None - return chain([self], self.binding.get_subexprs()) - - @classmethod - def _new_bind(cls: Type[SelfType], binding: Binding) -> SelfType: - """Returns a clone of this object with the specified binding. Discards existing binding / init data.""" - clone: SelfType = cls() - clone.binding = binding - return clone - - def _bind(self: SelfType, binding: Binding) -> SelfType: - """Returns a clone of this object with the specified binding. This object must be unbound.""" - assert not self._is_bound() - assert builder.get_enclosing_block() is self._context, f"can't clone in original context {self._context} to different new context {builder.get_enclosing_block()}" - if not isinstance(binding, ParamBinding): - assert self.initializer is None, "Only Parameters may have initializers" - clone: SelfType = type(self)(self.initializer) - clone.binding = binding - return clone - - def _is_bound(self) -> bool: - return self.binding is not None and self.binding.is_bound() - - def _populate_expr_proto(self, pb: edgir.ValueExpr, ref_map: Refable.RefMapType) -> None: - assert self.binding is not None - self.binding.populate_expr_proto(pb, self, ref_map) - - # for now we define that all constraints can be checked for equivalence - @override - def __eq__(self: SelfType, other: ConstraintExprCastable) -> BoolExpr: #type: ignore - # TODO: avoid creating excess BoolExpr - return BoolExpr()._bind(BinaryOpBinding(self, self._to_expr_type(other), EqOp.eq)) - - -BoolLike = Union[bool, 'BoolExpr'] + """Base class for constraint expressions. Basically a container for operations. + Actual meaning is held in the Binding. + """ + + _CASTABLE_TYPES: Tuple[Type[CastableType], ...] # for use in ininstance(), excluding self-cls + + @override + def __repr__(self) -> str: + if self.binding is not None and self.initializer is not None: + return f"{super().__repr__()}({self.binding})={self.initializer}" + if self.initializer is not None: + return f"{super().__repr__()}={self.initializer}" + if self.binding is not None: + return f"{super().__repr__()}({self.binding})" + else: + return f"{super().__repr__()}" + + @classmethod + @abstractmethod + def _to_expr_type(cls: Type[SelfType], input: CastableType) -> SelfType: + """Casts the input from an equivalent-type to the self-type.""" + raise NotImplementedError + + @classmethod + @abstractmethod + def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: + """Returns the protobuf for the definition of this parameter. Must have ParamBinding / ParamVariableBinding""" + raise NotImplementedError + + @classmethod + @abstractmethod + def _from_lit(cls, pb: edgir.ValueLit) -> WrappedType: + """Parses the literal / wrapped type from a ValueLit proto.""" + raise NotImplementedError + + def __init__(self: SelfType, initializer: Optional[Union[SelfType, WrappedType]] = None): + self.binding: Optional[Binding] = None + if isinstance(initializer, type(self)) and not initializer._is_bound(): # model passed in + self.initializer: Optional[SelfType] = initializer.initializer + elif initializer is None: + self.initializer = None + else: + self.initializer = self._to_expr_type(initializer) + self._context: Optional[BaseBlock] = builder.get_enclosing_block() + + def _get_exprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]: + assert self.binding is not None + return chain([self], self.binding.get_subexprs()) + + @classmethod + def _new_bind(cls: Type[SelfType], binding: Binding) -> SelfType: + """Returns a clone of this object with the specified binding. Discards existing binding / init data.""" + clone: SelfType = cls() + clone.binding = binding + return clone + + def _bind(self: SelfType, binding: Binding) -> SelfType: + """Returns a clone of this object with the specified binding. This object must be unbound.""" + assert not self._is_bound() + assert ( + builder.get_enclosing_block() is self._context + ), f"can't clone in original context {self._context} to different new context {builder.get_enclosing_block()}" + if not isinstance(binding, ParamBinding): + assert self.initializer is None, "Only Parameters may have initializers" + clone: SelfType = type(self)(self.initializer) + clone.binding = binding + return clone + + def _is_bound(self) -> bool: + return self.binding is not None and self.binding.is_bound() + + def _populate_expr_proto(self, pb: edgir.ValueExpr, ref_map: Refable.RefMapType) -> None: + assert self.binding is not None + self.binding.populate_expr_proto(pb, self, ref_map) + + # for now we define that all constraints can be checked for equivalence + @override + def __eq__(self: SelfType, other: ConstraintExprCastable) -> BoolExpr: # type: ignore + # TODO: avoid creating excess BoolExpr + return BoolExpr()._bind(BinaryOpBinding(self, self._to_expr_type(other), EqOp.eq)) + + +BoolLike = Union[bool, "BoolExpr"] + + class BoolExpr(ConstraintExpr[bool, BoolLike]): - """Boolean expression, can be used as a constraint""" - - _CASTABLE_TYPES = (bool, ) - - @classmethod - @override - def _to_expr_type(cls, input: BoolLike) -> BoolExpr: - if isinstance(input, BoolExpr): - assert input._is_bound() - return input - elif isinstance(input, bool): - return BoolExpr()._bind(BoolLiteralBinding(input)) - else: - raise ValueError("unexpected type for %s of %s, expected BoolLike" % (input, type(input))) - - @classmethod - @override - def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: - pb.boolean.SetInParent() - - @classmethod - @override - def _from_lit(cls, pb: edgir.ValueLit) -> bool: - assert pb.HasField('boolean') - return pb.boolean.val - - @classmethod - def _create_binary_op(cls, - lhs: BoolExpr, - rhs: BoolExpr, - op: Union[NumericOp, BoolOp, EqOp, OrdOp, RangeSetOp]) -> BoolExpr: # TODO dedup w/ NumLike - """Creates a new expression that is the result of a binary operation on inputs""" - if type(lhs) != type(rhs): - raise TypeError(f"op args must be of same type, " - f"got lhs={lhs} of type {type(lhs)} and rhs={rhs} of type {type(rhs)}") - - assert lhs._is_bound() and rhs._is_bound() - return lhs._new_bind(BinaryOpBinding(lhs, rhs, op)) - - def __and__(self, rhs: BoolLike) -> BoolExpr: - return self._create_binary_op(self, self._to_expr_type(rhs), BoolOp.op_and) - - def __rand__(self, lhs: BoolLike) -> BoolExpr: - return self._create_binary_op(self._to_expr_type(lhs), self, BoolOp.op_and) - - def __or__(self, rhs: BoolLike) -> BoolExpr: - return self._create_binary_op(self, self._to_expr_type(rhs), BoolOp.op_or) - - def __ror__(self, lhs: BoolLike) -> BoolExpr: - return self._create_binary_op(self._to_expr_type(lhs), self, BoolOp.op_or) - - def __xor__(self, rhs: BoolLike) -> BoolExpr: - return self._create_binary_op(self, self._to_expr_type(rhs), BoolOp.op_xor) - - def __rxor__(self, lhs: BoolLike) -> BoolExpr: - return self._create_binary_op(self._to_expr_type(lhs), self, BoolOp.op_xor) - - def implies(self, target: BoolLike) -> BoolExpr: - return self._create_binary_op(self, self._to_expr_type(target), BoolOp.implies) - - def __invert__(self) -> BoolExpr: - return self._new_bind(UnaryOpBinding(self, BoolOp.op_not)) - - # does not seem possible to restrict type-params of type vars pre-Python3.12 - # so where a single cast is needed we're stuck with Any - IteType = TypeVar('IteType', bound=ConstraintExpr) - @overload - def then_else(self, then_val: IteType, else_val: IteType) -> IteType: ... # optional strongest-typed version - @overload - def then_else(self, then_val: IteType, else_val: Any) -> IteType: ... - @overload - def then_else(self, then_val: Any, else_val: IteType) -> IteType: ... - - def then_else(self, then_val: Any, else_val: Any) -> ConstraintExpr: - if isinstance(then_val, ConstraintExpr): - else_val = then_val._to_expr_type(else_val) - elif isinstance(else_val, ConstraintExpr): - then_val = else_val._to_expr_type(then_val) - else: - raise ValueError("either then_val or else_val must be ConstraintExpr, TODO support dual-casting") - assert self._is_bound() and then_val._is_bound() and else_val._is_bound() - return then_val._new_bind(IfThenElseBinding(self, then_val, else_val)) # type: ignore - - -NumLikeSelfType = TypeVar('NumLikeSelfType', bound='NumLikeExpr') -NumLikeCastable = TypeVar('NumLikeCastable', default=Any) # should include the self type + """Boolean expression, can be used as a constraint""" + + _CASTABLE_TYPES = (bool,) + + @classmethod + @override + def _to_expr_type(cls, input: BoolLike) -> BoolExpr: + if isinstance(input, BoolExpr): + assert input._is_bound() + return input + elif isinstance(input, bool): + return BoolExpr()._bind(BoolLiteralBinding(input)) + else: + raise ValueError("unexpected type for %s of %s, expected BoolLike" % (input, type(input))) + + @classmethod + @override + def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: + pb.boolean.SetInParent() + + @classmethod + @override + def _from_lit(cls, pb: edgir.ValueLit) -> bool: + assert pb.HasField("boolean") + return pb.boolean.val + + @classmethod + def _create_binary_op( + cls, lhs: BoolExpr, rhs: BoolExpr, op: Union[NumericOp, BoolOp, EqOp, OrdOp, RangeSetOp] + ) -> BoolExpr: # TODO dedup w/ NumLike + """Creates a new expression that is the result of a binary operation on inputs""" + if type(lhs) != type(rhs): + raise TypeError( + f"op args must be of same type, " f"got lhs={lhs} of type {type(lhs)} and rhs={rhs} of type {type(rhs)}" + ) + + assert lhs._is_bound() and rhs._is_bound() + return lhs._new_bind(BinaryOpBinding(lhs, rhs, op)) + + def __and__(self, rhs: BoolLike) -> BoolExpr: + return self._create_binary_op(self, self._to_expr_type(rhs), BoolOp.op_and) + + def __rand__(self, lhs: BoolLike) -> BoolExpr: + return self._create_binary_op(self._to_expr_type(lhs), self, BoolOp.op_and) + + def __or__(self, rhs: BoolLike) -> BoolExpr: + return self._create_binary_op(self, self._to_expr_type(rhs), BoolOp.op_or) + + def __ror__(self, lhs: BoolLike) -> BoolExpr: + return self._create_binary_op(self._to_expr_type(lhs), self, BoolOp.op_or) + + def __xor__(self, rhs: BoolLike) -> BoolExpr: + return self._create_binary_op(self, self._to_expr_type(rhs), BoolOp.op_xor) + + def __rxor__(self, lhs: BoolLike) -> BoolExpr: + return self._create_binary_op(self._to_expr_type(lhs), self, BoolOp.op_xor) + + def implies(self, target: BoolLike) -> BoolExpr: + return self._create_binary_op(self, self._to_expr_type(target), BoolOp.implies) + + def __invert__(self) -> BoolExpr: + return self._new_bind(UnaryOpBinding(self, BoolOp.op_not)) + + # does not seem possible to restrict type-params of type vars pre-Python3.12 + # so where a single cast is needed we're stuck with Any + IteType = TypeVar("IteType", bound=ConstraintExpr) + + @overload + def then_else(self, then_val: IteType, else_val: IteType) -> IteType: ... # optional strongest-typed version + @overload + def then_else(self, then_val: IteType, else_val: Any) -> IteType: ... + @overload + def then_else(self, then_val: Any, else_val: IteType) -> IteType: ... + + def then_else(self, then_val: Any, else_val: Any) -> ConstraintExpr: + if isinstance(then_val, ConstraintExpr): + else_val = then_val._to_expr_type(else_val) + elif isinstance(else_val, ConstraintExpr): + then_val = else_val._to_expr_type(then_val) + else: + raise ValueError("either then_val or else_val must be ConstraintExpr, TODO support dual-casting") + assert self._is_bound() and then_val._is_bound() and else_val._is_bound() + return then_val._new_bind(IfThenElseBinding(self, then_val, else_val)) # type: ignore + + +NumLikeSelfType = TypeVar("NumLikeSelfType", bound="NumLikeExpr") +NumLikeCastable = TypeVar("NumLikeCastable", default=Any) # should include the self type + + class NumLikeExpr(ConstraintExpr[WrappedType, NumLikeCastable], Generic[WrappedType, NumLikeCastable]): - """Trait for numeric-like expressions, providing common arithmetic operations""" - - @classmethod - @abstractmethod - @override - def _to_expr_type(cls: Type[NumLikeSelfType], - input: Union[NumLikeSelfType, WrappedType, NumLikeCastable]) -> NumLikeSelfType: - """Casts the input from an equivalent-type to the self-type.""" - raise NotImplementedError - - @classmethod - def _create_unary_op(cls, - var: SelfType, - op: NumericOp) -> SelfType: - """Creates a new expression that is the result of a unary operation on the input""" - - assert var._is_bound() - return var._new_bind(UnaryOpBinding(var, op)) - - @classmethod - def _create_binary_op(cls, - lhs: SelfType, - rhs: SelfType, - op: Union[NumericOp, RangeSetOp]) -> SelfType: - """Creates a new expression that is the result of a binary operation on inputs""" - if type(lhs) != type(rhs): - raise TypeError(f"op args must be of same type, " - f"got lhs={lhs} of type {type(lhs)} and rhs={rhs} of type {type(rhs)}") - - assert lhs._is_bound() and rhs._is_bound() - return lhs._new_bind(BinaryOpBinding(lhs, rhs, op)) - - def __neg__(self: NumLikeSelfType) -> NumLikeSelfType: - return self._create_unary_op(self, NumericOp.negate) - - def __mul_inv__(self: NumLikeSelfType) -> NumLikeSelfType: - return self._create_unary_op(self, NumericOp.invert) - - def __add__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType: - if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__): - return self._create_binary_op(self, self._to_expr_type(rhs), NumericOp.add) - return NotImplemented - - def __radd__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType: - if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__): - return self._create_binary_op(self._to_expr_type(lhs), self, NumericOp.add) - return NotImplemented - - def __sub__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType: - if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__): - return self.__add__(self._to_expr_type(rhs).__neg__()) - return NotImplemented - - def __rsub__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType: - if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__): - return self.__neg__().__radd__(self._to_expr_type(lhs)) - return NotImplemented - - def __mul__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType: - if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__): - return self._create_binary_op(self, self._to_expr_type(rhs), NumericOp.mul) - return NotImplemented - - def __rmul__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType: - if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__): - return self._create_binary_op(self._to_expr_type(lhs), self, NumericOp.mul) - return NotImplemented - - def __truediv__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType: - if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__): - return self.__mul__(self._to_expr_type(rhs).__mul_inv__()) - return NotImplemented - - def __rtruediv__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType: - if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__): - return self.__mul_inv__().__mul__(self._to_expr_type(lhs)) - return NotImplemented - - @classmethod - def _create_bool_op(cls, - lhs: ConstraintExpr, - rhs: ConstraintExpr, - op: Union[BoolOp,EqOp,OrdOp]) -> BoolExpr: - if not isinstance(lhs, ConstraintExpr): - raise TypeError(f"op args must be of type ConstraintExpr, got {lhs} of type {type(lhs)}") - if not isinstance(rhs, ConstraintExpr): - raise TypeError(f"op args must be of type ConstraintExpr, got {rhs} of type {type(rhs)}") - assert lhs._is_bound() and rhs._is_bound() - return BoolExpr()._bind(BinaryOpBinding(lhs, rhs, op)) - - def __ne__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: #type: ignore - if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): - return self._create_bool_op(self, self._to_expr_type(other), EqOp.ne) - return NotImplemented - - def __gt__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: - if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): - return self._create_bool_op(self, self._to_expr_type(other), OrdOp.gt) - return NotImplemented - - def __ge__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: - if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): - return self._create_bool_op(self, self._to_expr_type(other), OrdOp.ge) - return NotImplemented - - def __lt__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: - if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): - return self._create_bool_op(self, self._to_expr_type(other), OrdOp.lt) - return NotImplemented - - def __le__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: - if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): - return self._create_bool_op(self, self._to_expr_type(other), OrdOp.le) - return NotImplemented - - -IntLike = Union['IntExpr', int] + """Trait for numeric-like expressions, providing common arithmetic operations""" + + @classmethod + @abstractmethod + @override + def _to_expr_type( + cls: Type[NumLikeSelfType], input: Union[NumLikeSelfType, WrappedType, NumLikeCastable] + ) -> NumLikeSelfType: + """Casts the input from an equivalent-type to the self-type.""" + raise NotImplementedError + + @classmethod + def _create_unary_op(cls, var: SelfType, op: NumericOp) -> SelfType: + """Creates a new expression that is the result of a unary operation on the input""" + + assert var._is_bound() + return var._new_bind(UnaryOpBinding(var, op)) + + @classmethod + def _create_binary_op(cls, lhs: SelfType, rhs: SelfType, op: Union[NumericOp, RangeSetOp]) -> SelfType: + """Creates a new expression that is the result of a binary operation on inputs""" + if type(lhs) != type(rhs): + raise TypeError( + f"op args must be of same type, " f"got lhs={lhs} of type {type(lhs)} and rhs={rhs} of type {type(rhs)}" + ) + + assert lhs._is_bound() and rhs._is_bound() + return lhs._new_bind(BinaryOpBinding(lhs, rhs, op)) + + def __neg__(self: NumLikeSelfType) -> NumLikeSelfType: + return self._create_unary_op(self, NumericOp.negate) + + def __mul_inv__(self: NumLikeSelfType) -> NumLikeSelfType: + return self._create_unary_op(self, NumericOp.invert) + + def __add__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType: + if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__): + return self._create_binary_op(self, self._to_expr_type(rhs), NumericOp.add) + return NotImplemented + + def __radd__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType: + if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__): + return self._create_binary_op(self._to_expr_type(lhs), self, NumericOp.add) + return NotImplemented + + def __sub__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType: + if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__): + return self.__add__(self._to_expr_type(rhs).__neg__()) + return NotImplemented + + def __rsub__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType: + if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__): + return self.__neg__().__radd__(self._to_expr_type(lhs)) + return NotImplemented + + def __mul__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType: + if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__): + return self._create_binary_op(self, self._to_expr_type(rhs), NumericOp.mul) + return NotImplemented + + def __rmul__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType: + if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__): + return self._create_binary_op(self._to_expr_type(lhs), self, NumericOp.mul) + return NotImplemented + + def __truediv__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType: + if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__): + return self.__mul__(self._to_expr_type(rhs).__mul_inv__()) + return NotImplemented + + def __rtruediv__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType: + if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__): + return self.__mul_inv__().__mul__(self._to_expr_type(lhs)) + return NotImplemented + + @classmethod + def _create_bool_op(cls, lhs: ConstraintExpr, rhs: ConstraintExpr, op: Union[BoolOp, EqOp, OrdOp]) -> BoolExpr: + if not isinstance(lhs, ConstraintExpr): + raise TypeError(f"op args must be of type ConstraintExpr, got {lhs} of type {type(lhs)}") + if not isinstance(rhs, ConstraintExpr): + raise TypeError(f"op args must be of type ConstraintExpr, got {rhs} of type {type(rhs)}") + assert lhs._is_bound() and rhs._is_bound() + return BoolExpr()._bind(BinaryOpBinding(lhs, rhs, op)) + + def __ne__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: # type: ignore + if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): + return self._create_bool_op(self, self._to_expr_type(other), EqOp.ne) + return NotImplemented + + def __gt__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: + if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): + return self._create_bool_op(self, self._to_expr_type(other), OrdOp.gt) + return NotImplemented + + def __ge__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: + if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): + return self._create_bool_op(self, self._to_expr_type(other), OrdOp.ge) + return NotImplemented + + def __lt__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: + if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): + return self._create_bool_op(self, self._to_expr_type(other), OrdOp.lt) + return NotImplemented + + def __le__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: + if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__): + return self._create_bool_op(self, self._to_expr_type(other), OrdOp.le) + return NotImplemented + + +IntLike = Union["IntExpr", int] + + class IntExpr(NumLikeExpr[int, IntLike]): - _CASTABLE_TYPES = (int, ) - - @classmethod - @override - def _to_expr_type(cls, input: IntLike) -> IntExpr: - if isinstance(input, IntExpr): - assert input._is_bound() - return input - elif isinstance(input, int): - return IntExpr()._bind(IntLiteralBinding(input)) - else: - raise TypeError(f"op arg to IntExpr must be IntLike, got {input} of type {type(input)}") - - @classmethod - @override - def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: - pb.integer.SetInParent() - - @classmethod - @override - def _from_lit(cls, pb: edgir.ValueLit) -> int: - assert pb.HasField('integer') - return pb.integer.val + _CASTABLE_TYPES = (int,) + + @classmethod + @override + def _to_expr_type(cls, input: IntLike) -> IntExpr: + if isinstance(input, IntExpr): + assert input._is_bound() + return input + elif isinstance(input, int): + return IntExpr()._bind(IntLiteralBinding(input)) + else: + raise TypeError(f"op arg to IntExpr must be IntLike, got {input} of type {type(input)}") + + @classmethod + @override + def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: + pb.integer.SetInParent() + + @classmethod + @override + def _from_lit(cls, pb: edgir.ValueLit) -> int: + assert pb.HasField("integer") + return pb.integer.val FloatLit = Union[float, int] -FloatLike = Union['FloatExpr', float, int] +FloatLike = Union["FloatExpr", float, int] + + class FloatExpr(NumLikeExpr[float, Union[FloatLike, IntExpr]]): - _CASTABLE_TYPES = (float, int) - - @classmethod - @override - def _to_expr_type(cls, input: Union[FloatLike, IntExpr]) -> FloatExpr: - if isinstance(input, FloatExpr): - assert input._is_bound() - return input - elif isinstance(input, IntExpr): - assert input._is_bound() and input.binding is not None - return FloatExpr()._bind(input.binding) - elif isinstance(input, int) or isinstance(input, float): - return FloatExpr()._bind(FloatLiteralBinding(input)) - else: - raise TypeError(f"op arg to FloatExpr must be FloatLike, got {input} of type {type(input)}") - - @classmethod - @override - def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: - pb.floating.SetInParent() - - @classmethod - @override - def _from_lit(cls, pb: edgir.ValueLit) -> float: - assert pb.HasField('floating') - return pb.floating.val - - def min(self, other: FloatLike) -> FloatExpr: - return self._create_binary_op(self._to_expr_type(other), self, RangeSetOp.min) - - def max(self, other: FloatLike) -> FloatExpr: - return self._create_binary_op(self._to_expr_type(other), self, RangeSetOp.max) - - -RangeLike = Union['RangeExpr', Range, Tuple[FloatLike, FloatLike]] + _CASTABLE_TYPES = (float, int) + + @classmethod + @override + def _to_expr_type(cls, input: Union[FloatLike, IntExpr]) -> FloatExpr: + if isinstance(input, FloatExpr): + assert input._is_bound() + return input + elif isinstance(input, IntExpr): + assert input._is_bound() and input.binding is not None + return FloatExpr()._bind(input.binding) + elif isinstance(input, int) or isinstance(input, float): + return FloatExpr()._bind(FloatLiteralBinding(input)) + else: + raise TypeError(f"op arg to FloatExpr must be FloatLike, got {input} of type {type(input)}") + + @classmethod + @override + def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: + pb.floating.SetInParent() + + @classmethod + @override + def _from_lit(cls, pb: edgir.ValueLit) -> float: + assert pb.HasField("floating") + return pb.floating.val + + def min(self, other: FloatLike) -> FloatExpr: + return self._create_binary_op(self._to_expr_type(other), self, RangeSetOp.min) + + def max(self, other: FloatLike) -> FloatExpr: + return self._create_binary_op(self._to_expr_type(other), self, RangeSetOp.max) + + +RangeLike = Union["RangeExpr", Range, Tuple[FloatLike, FloatLike]] + + class RangeExpr(NumLikeExpr[Range, Union[RangeLike, FloatLike, IntExpr]]): - # mypy doesn't like the unbounded tuple - _CASTABLE_TYPES = (float, int, FloatExpr, IntExpr, Range, tuple) # type: ignore - - # Some range literals for defaults - POSITIVE: Range = Range.from_lower(0.0) - NEGATIVE: Range = Range.from_upper(0.0) - ALL: Range = Range.all() - INF: Range = Range(float('inf'), float('inf')) - ZERO: Range = Range(0.0, 0.0) - EMPTY = Range(float('NaN'), float('NaN')) # special marker to define an empty range, which is subset-eq of any range - - @classmethod - @override - def _to_expr_type(cls, input: Union[RangeLike, FloatLike, IntLike]) -> RangeExpr: - if isinstance(input, RangeExpr): - assert input._is_bound() - return input - elif isinstance(input, (int, float, FloatExpr, IntExpr)): - expr = FloatExpr._to_expr_type(input) - return RangeExpr()._bind(RangeBuilderBinding(expr, expr)) - elif isinstance(input, tuple) and isinstance(input[0], (int, float)) and isinstance(input[1], (int, float)): - assert len(input) == 2 - return RangeExpr()._bind(RangeLiteralBinding(Range(input[0], input[1]))) - elif isinstance(input, Range): - return RangeExpr()._bind(RangeLiteralBinding(input)) - elif isinstance(input, tuple): - assert len(input) == 2 - return RangeExpr()._bind(RangeBuilderBinding( - FloatExpr._to_expr_type(input[0]), - FloatExpr._to_expr_type(input[1]) - )) - else: - raise TypeError(f"op arg to RangeExpr must be RangeLike, got {input} of type {type(input)}") - - @classmethod - @override - def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: - pb.range.SetInParent() - - @classmethod - @override - def _from_lit(cls, pb: edgir.ValueLit) -> Range: - assert pb.HasField('range') and pb.range.minimum.HasField('floating') and pb.range.maximum.HasField('floating') - return Range(pb.range.minimum.floating.val, pb.range.maximum.floating.val) - - @classmethod - @deprecated("Use shrink_multiply") - def cancel_multiply(cls, input_side: RangeLike, output_side: RangeLike) -> RangeExpr: - return RangeExpr._to_expr_type(output_side).shrink_multiply(input_side) - - def __init__(self, initializer: Optional[RangeLike] = None) -> None: - # must cast non-empty initializer type, because range supports wider initializers - # TODO and must ignore initializers of self-type (because model weirdness) - remove model support! - if initializer is not None and not isinstance(initializer, RangeExpr): - initializer = self._to_expr_type(initializer) - super().__init__(initializer) - self._lower = FloatExpr()._bind(UnaryOpBinding(self, RangeSetOp.min)) - self._upper = FloatExpr()._bind(UnaryOpBinding(self, RangeSetOp.max)) - - def _initializer_to(self, target: ConstraintExpr) -> BoolExpr: - # must also handle initializer mode (subset, superset) here - assert isinstance(target, type(self)), "target must be of same type" # TODO don't use isinstance - if self.initializer is None: - return BoolExpr._to_expr_type(True) - else: - return target == self.initializer - - def within(self, item: RangeLike) -> BoolExpr: - return self._create_bool_op(self, RangeExpr._to_expr_type(item), OrdOp.within) - - def contains(self, item: Union[RangeLike, FloatLike]) -> BoolExpr: - if isinstance(item, (RangeExpr, tuple, Range)): - return RangeExpr._to_expr_type(item).within(self) - elif isinstance(item, (int, float, FloatExpr, IntExpr)): - return self._create_bool_op(FloatExpr._to_expr_type(item), self, OrdOp.within) - - def intersect(self, other: Union[RangeLike, FloatLike]) -> RangeExpr: - return self._create_binary_op(self._to_expr_type(other), self, RangeSetOp.intersection) - - def hull(self, other: Union[RangeLike, FloatLike]) -> RangeExpr: - return self._create_binary_op(self._to_expr_type(other), self, RangeSetOp.hull) - - def lower(self) -> FloatExpr: - return self._lower - - def upper(self) -> FloatExpr: - return self._upper - - def center(self) -> FloatExpr: - return (self._lower + self._upper) / 2 - - @classmethod - def _create_range_float_binary_op(cls, - lhs: RangeExpr, - rhs: Union[RangeExpr, IntExpr, FloatExpr], - op: Union[NumericOp]) -> RangeExpr: - """Creates a new expression that is the result of a binary operation on inputs""" - if not isinstance(lhs, RangeExpr): - raise TypeError(f"range mul and div lhs must be range type, " - f"got lhs={lhs} of type {type(lhs)} and rhs={rhs} of type {type(rhs)}") - - if not isinstance(rhs, (RangeExpr, FloatExpr, IntExpr)): - raise TypeError(f"range mul and div rhs must be range or float or int type, " - f"got lhs={lhs} of type {type(lhs)} and rhs={rhs} of type {type(rhs)}") - - assert lhs._is_bound() and rhs._is_bound() - return lhs._new_bind(BinaryOpBinding(lhs, rhs, op)) - - def shrink_multiply(self, contributing: RangeLike) -> RangeExpr: - """RangeExpr version of Range.shrink_multiply. - See docs for Range.shrink_multiply.""" - return RangeExpr._create_binary_op(self, self._to_expr_type(contributing), NumericOp.shrink_mul) - - def abs(self) -> RangeExpr: - """Returns a RangeExpr that is the absolute value of this. - Intuitively, this returns a range that contains the absolute value of every point in the input.""" - return (self.lower() < 0).then_else( - (self.upper() < 0).then_else( - self._to_expr_type((0 - self.upper(), 0 - self.lower())), # both smaller than zero, invert everything - self._to_expr_type((0, self.upper().max(0 - self.lower()))) # range crosses zero - ), - self # lower > 0 and by extension upper > 0, leave as-is - ) - - -StringLike = Union['StringExpr', str] + # mypy doesn't like the unbounded tuple + _CASTABLE_TYPES = (float, int, FloatExpr, IntExpr, Range, tuple) # type: ignore + + # Some range literals for defaults + POSITIVE: Range = Range.from_lower(0.0) + NEGATIVE: Range = Range.from_upper(0.0) + ALL: Range = Range.all() + INF: Range = Range(float("inf"), float("inf")) + ZERO: Range = Range(0.0, 0.0) + EMPTY = Range( + float("NaN"), float("NaN") + ) # special marker to define an empty range, which is subset-eq of any range + + @classmethod + @override + def _to_expr_type(cls, input: Union[RangeLike, FloatLike, IntLike]) -> RangeExpr: + if isinstance(input, RangeExpr): + assert input._is_bound() + return input + elif isinstance(input, (int, float, FloatExpr, IntExpr)): + expr = FloatExpr._to_expr_type(input) + return RangeExpr()._bind(RangeBuilderBinding(expr, expr)) + elif isinstance(input, tuple) and isinstance(input[0], (int, float)) and isinstance(input[1], (int, float)): + assert len(input) == 2 + return RangeExpr()._bind(RangeLiteralBinding(Range(input[0], input[1]))) + elif isinstance(input, Range): + return RangeExpr()._bind(RangeLiteralBinding(input)) + elif isinstance(input, tuple): + assert len(input) == 2 + return RangeExpr()._bind( + RangeBuilderBinding(FloatExpr._to_expr_type(input[0]), FloatExpr._to_expr_type(input[1])) + ) + else: + raise TypeError(f"op arg to RangeExpr must be RangeLike, got {input} of type {type(input)}") + + @classmethod + @override + def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: + pb.range.SetInParent() + + @classmethod + @override + def _from_lit(cls, pb: edgir.ValueLit) -> Range: + assert pb.HasField("range") and pb.range.minimum.HasField("floating") and pb.range.maximum.HasField("floating") + return Range(pb.range.minimum.floating.val, pb.range.maximum.floating.val) + + @classmethod + @deprecated("Use shrink_multiply") + def cancel_multiply(cls, input_side: RangeLike, output_side: RangeLike) -> RangeExpr: + return RangeExpr._to_expr_type(output_side).shrink_multiply(input_side) + + def __init__(self, initializer: Optional[RangeLike] = None) -> None: + # must cast non-empty initializer type, because range supports wider initializers + # TODO and must ignore initializers of self-type (because model weirdness) - remove model support! + if initializer is not None and not isinstance(initializer, RangeExpr): + initializer = self._to_expr_type(initializer) + super().__init__(initializer) + self._lower = FloatExpr()._bind(UnaryOpBinding(self, RangeSetOp.min)) + self._upper = FloatExpr()._bind(UnaryOpBinding(self, RangeSetOp.max)) + + def _initializer_to(self, target: ConstraintExpr) -> BoolExpr: + # must also handle initializer mode (subset, superset) here + assert isinstance(target, type(self)), "target must be of same type" # TODO don't use isinstance + if self.initializer is None: + return BoolExpr._to_expr_type(True) + else: + return target == self.initializer + + def within(self, item: RangeLike) -> BoolExpr: + return self._create_bool_op(self, RangeExpr._to_expr_type(item), OrdOp.within) + + def contains(self, item: Union[RangeLike, FloatLike]) -> BoolExpr: + if isinstance(item, (RangeExpr, tuple, Range)): + return RangeExpr._to_expr_type(item).within(self) + elif isinstance(item, (int, float, FloatExpr, IntExpr)): + return self._create_bool_op(FloatExpr._to_expr_type(item), self, OrdOp.within) + + def intersect(self, other: Union[RangeLike, FloatLike]) -> RangeExpr: + return self._create_binary_op(self._to_expr_type(other), self, RangeSetOp.intersection) + + def hull(self, other: Union[RangeLike, FloatLike]) -> RangeExpr: + return self._create_binary_op(self._to_expr_type(other), self, RangeSetOp.hull) + + def lower(self) -> FloatExpr: + return self._lower + + def upper(self) -> FloatExpr: + return self._upper + + def center(self) -> FloatExpr: + return (self._lower + self._upper) / 2 + + @classmethod + def _create_range_float_binary_op( + cls, lhs: RangeExpr, rhs: Union[RangeExpr, IntExpr, FloatExpr], op: Union[NumericOp] + ) -> RangeExpr: + """Creates a new expression that is the result of a binary operation on inputs""" + if not isinstance(lhs, RangeExpr): + raise TypeError( + f"range mul and div lhs must be range type, " + f"got lhs={lhs} of type {type(lhs)} and rhs={rhs} of type {type(rhs)}" + ) + + if not isinstance(rhs, (RangeExpr, FloatExpr, IntExpr)): + raise TypeError( + f"range mul and div rhs must be range or float or int type, " + f"got lhs={lhs} of type {type(lhs)} and rhs={rhs} of type {type(rhs)}" + ) + + assert lhs._is_bound() and rhs._is_bound() + return lhs._new_bind(BinaryOpBinding(lhs, rhs, op)) + + def shrink_multiply(self, contributing: RangeLike) -> RangeExpr: + """RangeExpr version of Range.shrink_multiply. + See docs for Range.shrink_multiply.""" + return RangeExpr._create_binary_op(self, self._to_expr_type(contributing), NumericOp.shrink_mul) + + def abs(self) -> RangeExpr: + """Returns a RangeExpr that is the absolute value of this. + Intuitively, this returns a range that contains the absolute value of every point in the input.""" + return (self.lower() < 0).then_else( + (self.upper() < 0).then_else( + self._to_expr_type((0 - self.upper(), 0 - self.lower())), # both smaller than zero, invert everything + self._to_expr_type((0, self.upper().max(0 - self.lower()))), # range crosses zero + ), + self, # lower > 0 and by extension upper > 0, leave as-is + ) + + +StringLike = Union["StringExpr", str] + + class StringExpr(ConstraintExpr[str, StringLike]): - """String expression, can be used as a constraint""" - - _CASTABLE_TYPES = (str, ) - - @classmethod - @override - def _to_expr_type(cls, input: StringLike) -> StringExpr: - if isinstance(input, StringExpr): - assert input._is_bound() - return input - elif isinstance(input, str): - return StringExpr()._bind(StringLiteralBinding(input)) - else: - raise ValueError("unexpected type for %s of %s, expected StringLike" % (input, type(input))) - - @classmethod - @override - def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: - pb.text.SetInParent() - - @classmethod - @override - def _from_lit(cls, pb: edgir.ValueLit) -> str: - assert pb.HasField('text') - return pb.text.val - - def _is_lit(self) -> bool: - assert self._is_bound() - return isinstance(self.binding, StringLiteralBinding) - - def __add__(self, rhs: StringLike) -> StringExpr: - rhs_typed = self._to_expr_type(rhs) - assert self._is_bound() and rhs_typed._is_bound() - return self._new_bind(BinaryOpBinding(self, rhs_typed, NumericOp.add)) - - def __radd__(self, lhs: StringLike) -> StringExpr: - lhs_typed = self._to_expr_type(lhs) - assert lhs_typed._is_bound() and self._is_bound() - return self._new_bind(BinaryOpBinding(self, lhs_typed, NumericOp.add)) + """String expression, can be used as a constraint""" + + _CASTABLE_TYPES = (str,) + + @classmethod + @override + def _to_expr_type(cls, input: StringLike) -> StringExpr: + if isinstance(input, StringExpr): + assert input._is_bound() + return input + elif isinstance(input, str): + return StringExpr()._bind(StringLiteralBinding(input)) + else: + raise ValueError("unexpected type for %s of %s, expected StringLike" % (input, type(input))) + + @classmethod + @override + def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: + pb.text.SetInParent() + + @classmethod + @override + def _from_lit(cls, pb: edgir.ValueLit) -> str: + assert pb.HasField("text") + return pb.text.val + + def _is_lit(self) -> bool: + assert self._is_bound() + return isinstance(self.binding, StringLiteralBinding) + + def __add__(self, rhs: StringLike) -> StringExpr: + rhs_typed = self._to_expr_type(rhs) + assert self._is_bound() and rhs_typed._is_bound() + return self._new_bind(BinaryOpBinding(self, rhs_typed, NumericOp.add)) + + def __radd__(self, lhs: StringLike) -> StringExpr: + lhs_typed = self._to_expr_type(lhs) + assert lhs_typed._is_bound() and self._is_bound() + return self._new_bind(BinaryOpBinding(self, lhs_typed, NumericOp.add)) class AssignExpr(ConstraintExpr[None, None]): - """Assignment expression, should be an internal type""" - @classmethod - @override - def _to_expr_type(cls, input: Any) -> NoReturn: - raise ValueError("can't convert to AssignExpr") + """Assignment expression, should be an internal type""" + + @classmethod + @override + def _to_expr_type(cls, input: Any) -> NoReturn: + raise ValueError("can't convert to AssignExpr") - @classmethod - @override - def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: - raise ValueError("can't create parameter from AssignExpr") + @classmethod + @override + def _populate_decl_proto(cls, pb: edgir.ValInit) -> None: + raise ValueError("can't create parameter from AssignExpr") - @classmethod - @override - def _from_lit(cls, pb: edgir.ValueLit) -> NoReturn: - raise ValueError("can't unpack AssignExpr") + @classmethod + @override + def _from_lit(cls, pb: edgir.ValueLit) -> NoReturn: + raise ValueError("can't unpack AssignExpr") - def _is_lit(self) -> NoReturn: - raise ValueError("can't have literal AssignExpr") + def _is_lit(self) -> NoReturn: + raise ValueError("can't have literal AssignExpr") # TODO actually implement dimensional analysis and units type checking class RangeConstructor: - def __init__(self, tolerance: float, scale: float = 1, units: str = '') -> None: - self.tolerance = tolerance - self.scale = scale - self.units = units - - @overload - def __rmul__(self, other: FloatLike) -> RangeExpr: ... - @overload - def __rmul__(self, other: RangeLike) -> RangeExpr: ... - - def __rmul__(self, other: Union[FloatLike, RangeLike]) -> RangeExpr: - if isinstance(other, (int, float)): - values = [ - other * self.scale * (1 - self.tolerance), - other * self.scale * (1 + self.tolerance) - ] - elif isinstance(other, tuple) and isinstance(other[0], (int, float)) and isinstance(other[1], (int, float)): - assert other[0] <= other[1] - if other[0] < 0 and other[1] < 0: - values = [ - other[0] * self.scale * (1 + self.tolerance), - other[1] * self.scale * (1 - self.tolerance) - ] - elif other[0] < 0 <= other[1]: - values = [ - other[0] * self.scale * (1 + self.tolerance), - other[1] * self.scale * (1 + self.tolerance) - ] - elif other[0] >= 0 and other[1] >= 0: - values = [ - other[0] * self.scale * (1 - self.tolerance), - other[1] * self.scale * (1 + self.tolerance) - ] - else: - assert False, "range tolerance broken =(" - assert values[0] <= values[1] - else: - raise TypeError(f"expected Float or Range Literal, got {other} of type {type(other)}") - return RangeExpr._to_expr_type((min(values), max(values))) + def __init__(self, tolerance: float, scale: float = 1, units: str = "") -> None: + self.tolerance = tolerance + self.scale = scale + self.units = units + + @overload + def __rmul__(self, other: FloatLike) -> RangeExpr: ... + @overload + def __rmul__(self, other: RangeLike) -> RangeExpr: ... + + def __rmul__(self, other: Union[FloatLike, RangeLike]) -> RangeExpr: + if isinstance(other, (int, float)): + values = [other * self.scale * (1 - self.tolerance), other * self.scale * (1 + self.tolerance)] + elif isinstance(other, tuple) and isinstance(other[0], (int, float)) and isinstance(other[1], (int, float)): + assert other[0] <= other[1] + if other[0] < 0 and other[1] < 0: + values = [other[0] * self.scale * (1 + self.tolerance), other[1] * self.scale * (1 - self.tolerance)] + elif other[0] < 0 <= other[1]: + values = [other[0] * self.scale * (1 + self.tolerance), other[1] * self.scale * (1 + self.tolerance)] + elif other[0] >= 0 and other[1] >= 0: + values = [other[0] * self.scale * (1 - self.tolerance), other[1] * self.scale * (1 + self.tolerance)] + else: + assert False, "range tolerance broken =(" + assert values[0] <= values[1] + else: + raise TypeError(f"expected Float or Range Literal, got {other} of type {type(other)}") + return RangeExpr._to_expr_type((min(values), max(values))) class LiteralConstructor: - def __init__(self, scale: float = 1, units: str = ''): - self.scale = scale - self.units = units - - def __call__(self, *, tol: Optional[float]=None) -> RangeConstructor: - if tol is None: - raise ValueError("requires tol, use without parens to create un-toleranced literal") - return RangeConstructor(tol, self.scale, self.units) - - @overload - def __rmul__(self, other: float) -> FloatExpr: ... - # can't use RangeLike directly because it overlaps with FloatLike - @overload - def __rmul__(self, other: Union[Range, Tuple[float, float]]) -> RangeExpr: ... - - def __rmul__(self, other: Union[float, Range, Tuple[float, float]]) -> Union[FloatExpr, RangeExpr]: - if isinstance(other, (int, float)): - return FloatExpr._to_expr_type(other * self.scale) - elif isinstance(other, Range): - return RangeExpr._to_expr_type(other * self.scale) - elif isinstance(other, tuple) and isinstance(other[0], (int, float, IntExpr, FloatExpr)) \ - and isinstance(other[1], (int, float, IntExpr, FloatExpr)): - return RangeExpr._to_expr_type((other[0] * self.scale, other[1] * self.scale)) - else: - raise TypeError(f"expected Float or Range Literal, got {other} of type {type(other)}") + def __init__(self, scale: float = 1, units: str = ""): + self.scale = scale + self.units = units + + def __call__(self, *, tol: Optional[float] = None) -> RangeConstructor: + if tol is None: + raise ValueError("requires tol, use without parens to create un-toleranced literal") + return RangeConstructor(tol, self.scale, self.units) + + @overload + def __rmul__(self, other: float) -> FloatExpr: ... + + # can't use RangeLike directly because it overlaps with FloatLike + @overload + def __rmul__(self, other: Union[Range, Tuple[float, float]]) -> RangeExpr: ... + + def __rmul__(self, other: Union[float, Range, Tuple[float, float]]) -> Union[FloatExpr, RangeExpr]: + if isinstance(other, (int, float)): + return FloatExpr._to_expr_type(other * self.scale) + elif isinstance(other, Range): + return RangeExpr._to_expr_type(other * self.scale) + elif ( + isinstance(other, tuple) + and isinstance(other[0], (int, float, IntExpr, FloatExpr)) + and isinstance(other[1], (int, float, IntExpr, FloatExpr)) + ): + return RangeExpr._to_expr_type((other[0] * self.scale, other[1] * self.scale)) + else: + raise TypeError(f"expected Float or Range Literal, got {other} of type {type(other)}") diff --git a/edg/core/Core.py b/edg/core/Core.py index c8f6d05e0..45013f346 100644 --- a/edg/core/Core.py +++ b/edg/core/Core.py @@ -11,305 +11,328 @@ from .IdentitySet import IdentitySet -ElementType = TypeVar('ElementType') +ElementType = TypeVar("ElementType") + + class SubElementDict(Generic[ElementType]): - def __init__(self, anon_prefix: Optional[str]=None) -> None: - self.anons = IdentitySet[ElementType]() # TODO this should be order-preserving? - self.anon_prefix = anon_prefix - self.container: Dict[str, ElementType] = {} - self.names = IdentityDict[ElementType, str]() # TODO unify w/ container? - self.keys_list: List[str] = [] - self.closed = False - - def register(self, item: ElementType) -> ElementType: - assert not self.closed, "adding items after closed" - self.anons.add(item) - return item - - def add_element(self, name: str, item: Any) -> None: - assert not self.closed, "naming items after closed" - assert item in self.anons, f"attempted to add {name}={item}, but did not pre-register" - self.anons.remove(item) - assert name not in self.container, f"attempted to reassign {name}={item}" - - self.container[name] = item - self.names[item] = name - self.keys_list.append(name) - - # TODO should this be automatically called? - def finalize(self) -> None: - if self.closed: - return - if self.anon_prefix is None: - assert not self.anons, f"can't have unnamed objects: {self.anons}" - else: - for id, elt in enumerate(self.anons): - name = f'{self.anon_prefix}_{id}' - assert name not in self.container, f"duplicate name {name}" - self.container[name] = elt - self.names[elt] = name + def __init__(self, anon_prefix: Optional[str] = None) -> None: + self.anons = IdentitySet[ElementType]() # TODO this should be order-preserving? + self.anon_prefix = anon_prefix + self.container: Dict[str, ElementType] = {} + self.names = IdentityDict[ElementType, str]() # TODO unify w/ container? + self.keys_list: List[str] = [] + self.closed = False + + def register(self, item: ElementType) -> ElementType: + assert not self.closed, "adding items after closed" + self.anons.add(item) + return item + + def add_element(self, name: str, item: Any) -> None: + assert not self.closed, "naming items after closed" + assert item in self.anons, f"attempted to add {name}={item}, but did not pre-register" + self.anons.remove(item) + assert name not in self.container, f"attempted to reassign {name}={item}" + + self.container[name] = item + self.names[item] = name self.keys_list.append(name) - self.anons = IdentitySet[ElementType]() # TODO needs clear operation - def all_values_temp(self) -> Iterable[ElementType]: # TODO needs better API name, reconcile w/ values? - return list(self.container.values()) + list(self.anons) + # TODO should this be automatically called? + def finalize(self) -> None: + if self.closed: + return + if self.anon_prefix is None: + assert not self.anons, f"can't have unnamed objects: {self.anons}" + else: + for id, elt in enumerate(self.anons): + name = f"{self.anon_prefix}_{id}" + assert name not in self.container, f"duplicate name {name}" + self.container[name] = elt + self.names[elt] = name + self.keys_list.append(name) + self.anons = IdentitySet[ElementType]() # TODO needs clear operation + + def all_values_temp(self) -> Iterable[ElementType]: # TODO needs better API name, reconcile w/ values? + return list(self.container.values()) + list(self.anons) - # TODO the below allows this to be used like a dict, is this a good idea? - # TODO should these make sure the dict is closed? - def items(self) -> ItemsView[str, ElementType]: - return self.container.items() + # TODO the below allows this to be used like a dict, is this a good idea? + # TODO should these make sure the dict is closed? + def items(self) -> ItemsView[str, ElementType]: + return self.container.items() - def items_ordered(self) -> Iterable[Tuple[str, ElementType]]: - return [(key, self.container[key]) for key in self.keys_list] + def items_ordered(self) -> Iterable[Tuple[str, ElementType]]: + return [(key, self.container[key]) for key in self.keys_list] - def keys_ordered(self) -> Iterable[str]: # TODO should this clone? - return self.keys_list + def keys_ordered(self) -> Iterable[str]: # TODO should this clone? + return self.keys_list - def values(self) -> ValuesView[ElementType]: - return self.container.values() + def values(self) -> ValuesView[ElementType]: + return self.container.values() - def __getitem__(self, item: str) -> ElementType: - return self.container[item] + def __getitem__(self, item: str) -> ElementType: + return self.container[item] - def __contains__(self, item: str) -> bool: - return item in self.container + def __contains__(self, item: str) -> bool: + return item in self.container + + def name_of(self, elt: ElementType) -> Optional[str]: + if elt in self.names: + return self.names[elt] + else: + return None - def name_of(self, elt: ElementType) -> Optional[str]: - if elt in self.names: - return self.names[elt] - else: - return None class SubElementManager: - def __init__(self) -> None: - self.dicts: List[Tuple[Union[Type[Any], Tuple[Type[Any], ...]], SubElementDict[Any]]] = [] - self.aliases = IdentityDict[Any, Any]() - - def new_dict(self, filter_type: Union[Type[ElementType], Tuple[Type[ElementType], ...]], - anon_prefix: Optional[str] = None) -> SubElementDict[ElementType]: - sub_dict = SubElementDict[ElementType](anon_prefix) - self.dicts.append((filter_type, sub_dict)) - return sub_dict - - def add_alias(self, src: Any, target: Any) -> None: - self.aliases[src] = target - - def add_element(self, name: str, item: Any) -> None: - if isinstance(item, ElementDict): - item._set_parent((name, self)) - else: - assigned = [] - for (tpe, dict) in self.dicts: - if item in dict.anons: - dict.add_element(name, item) - assigned.append(dict) - else: # require not conflicting name, or direct reassignment - assert name not in dict.container or dict.names.get(item, None) == name, f"duplicate name {name}" - assert len(assigned) <= 1, f"assigned {item} to multiple SubElementDict {assigned}" - - def name_of(self, item: Any) -> Optional[str]: - if item in self.aliases: - item = self.aliases[item] - name_candidates = [sub_dict.name_of(item) for sub_dict_type, sub_dict in self.dicts] - name_candidates_filtered = [name_candidate for name_candidate in name_candidates if name_candidate is not None] - assert len(name_candidates_filtered) <= 1, f"more than 1 name candidates {name_candidates} for {item}" - if not name_candidates_filtered: - return None - else: - return name_candidates_filtered[0] + def __init__(self) -> None: + self.dicts: List[Tuple[Union[Type[Any], Tuple[Type[Any], ...]], SubElementDict[Any]]] = [] + self.aliases = IdentityDict[Any, Any]() + + def new_dict( + self, filter_type: Union[Type[ElementType], Tuple[Type[ElementType], ...]], anon_prefix: Optional[str] = None + ) -> SubElementDict[ElementType]: + sub_dict = SubElementDict[ElementType](anon_prefix) + self.dicts.append((filter_type, sub_dict)) + return sub_dict + + def add_alias(self, src: Any, target: Any) -> None: + self.aliases[src] = target + + def add_element(self, name: str, item: Any) -> None: + if isinstance(item, ElementDict): + item._set_parent((name, self)) + else: + assigned = [] + for tpe, dict in self.dicts: + if item in dict.anons: + dict.add_element(name, item) + assigned.append(dict) + else: # require not conflicting name, or direct reassignment + assert name not in dict.container or dict.names.get(item, None) == name, f"duplicate name {name}" + assert len(assigned) <= 1, f"assigned {item} to multiple SubElementDict {assigned}" + + def name_of(self, item: Any) -> Optional[str]: + if item in self.aliases: + item = self.aliases[item] + name_candidates = [sub_dict.name_of(item) for sub_dict_type, sub_dict in self.dicts] + name_candidates_filtered = [name_candidate for name_candidate in name_candidates if name_candidate is not None] + assert len(name_candidates_filtered) <= 1, f"more than 1 name candidates {name_candidates} for {item}" + if not name_candidates_filtered: + return None + else: + return name_candidates_filtered[0] class ElementDict(Generic[ElementType]): - """Dict that contains sub-elements, basically a dict with a hook that notifies its parent when items are added.""" - # TODO also have a KeyType? Perhaps enforce type bounds, eg must be str-able? - def __init__(self) -> None: - self.container: Dict[Union[str, int], ElementType] = {} - self._parent: Optional[Tuple[str, SubElementManager]] = None # name prefix, pointer to top level + """Dict that contains sub-elements, basically a dict with a hook that notifies its parent when items are added.""" + + # TODO also have a KeyType? Perhaps enforce type bounds, eg must be str-able? + def __init__(self) -> None: + self.container: Dict[Union[str, int], ElementType] = {} + self._parent: Optional[Tuple[str, SubElementManager]] = None # name prefix, pointer to top level - def _set_parent(self, parent: Tuple[str, SubElementManager]) -> None: - self._parent = parent + def _set_parent(self, parent: Tuple[str, SubElementManager]) -> None: + self._parent = parent - def __setitem__(self, key: Union[str, int], value: ElementType) -> None: - assert self._parent is not None - self._parent[1].add_element(f"{self._parent[0]}[{key}]", value) # TODO perhaps some kind of check to make sure this is required? - self.container[key] = value + def __setitem__(self, key: Union[str, int], value: ElementType) -> None: + assert self._parent is not None + self._parent[1].add_element( + f"{self._parent[0]}[{key}]", value + ) # TODO perhaps some kind of check to make sure this is required? + self.container[key] = value - def __getitem__(self, item: Union[str, int]) -> ElementType: - return self.container[item] + def __getitem__(self, item: Union[str, int]) -> ElementType: + return self.container[item] - def items(self) -> ItemsView[Union[str, int], ElementType]: - return self.container.items() + def items(self) -> ItemsView[Union[str, int], ElementType]: + return self.container.items() - def values(self) -> ValuesView[ElementType]: - return self.container.values() + def values(self) -> ValuesView[ElementType]: + return self.container.values() -class Refable(): - """Object that could be referenced into a edgir.LocalPath""" - RefMapType = IdentityDict['Refable', edgir.LocalPath] +class Refable: + """Object that could be referenced into a edgir.LocalPath""" - @override - def __repr__(self) -> str: - return "%s@%02x" % (self.__class__.__name__, (id(self)//4)&0xff) + RefMapType = IdentityDict["Refable", edgir.LocalPath] - @override - def __eq__(self, other: Any) -> None: # type: ignore - raise NotImplementedError(f"__eq__ reserved for DSL, attempted to compare {self} and {other}") + @override + def __repr__(self) -> str: + return "%s@%02x" % (self.__class__.__name__, (id(self) // 4) & 0xFF) - def __bool__(self) -> bool: - raise ValueError("bool-ing a Refable is almost certainly a mistake. " - "Note: 'and' and 'or' do not work on BoolExpr, use '&' or '|' instead.") + @override + def __eq__(self, other: Any) -> None: # type: ignore + raise NotImplementedError(f"__eq__ reserved for DSL, attempted to compare {self} and {other}") - def _create_ref_map(self, prefix: edgir.LocalPath = edgir.LocalPath()) -> RefMapType: - """Wrapper around _build_ref_map for top-level refmap construction.""" - ref_map = IdentityDict[Refable, edgir.LocalPath]() - self._build_ref_map(ref_map, prefix) - return ref_map + def __bool__(self) -> bool: + raise ValueError( + "bool-ing a Refable is almost certainly a mistake. " + "Note: 'and' and 'or' do not work on BoolExpr, use '&' or '|' instead." + ) - def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: - """Adds the references contained by this object to the parameter refmap.""" - ref_map[self] = prefix + def _create_ref_map(self, prefix: edgir.LocalPath = edgir.LocalPath()) -> RefMapType: + """Wrapper around _build_ref_map for top-level refmap construction.""" + ref_map = IdentityDict[Refable, edgir.LocalPath]() + self._build_ref_map(ref_map, prefix) + return ref_map + + def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: + """Adds the references contained by this object to the parameter refmap.""" + ref_map[self] = prefix class EltPropertiesBase: - """"Base type for properties associated with a particular block, that do not apply to subtypes""" - pass + """ "Base type for properties associated with a particular block, that do not apply to subtypes""" + + pass NonLibraryProperty = EltPropertiesBase() -NonLibraryType = TypeVar('NonLibraryType', bound=Type['LibraryElement']) +NonLibraryType = TypeVar("NonLibraryType", bound=Type["LibraryElement"]) + + def non_library(decorated: NonLibraryType) -> NonLibraryType: - decorated._elt_properties[(decorated, NonLibraryProperty)] = None - return decorated + decorated._elt_properties[(decorated, NonLibraryProperty)] = None + return decorated @non_library class LibraryElement(Refable): - """Defines a library element, which optionally contains other library elements.""" - _elt_properties: Dict[Tuple[Type[LibraryElement], EltPropertiesBase], Any] = {} - - @override - def __repr__(self) -> str: - return "%s@%02x" % (self._get_def_name(), (id(self) // 4) & 0xff) - - def __init__(self) -> None: - self._parent: Optional[LibraryElement] = None # set by binding, None means not bound - - self.manager = SubElementManager() - self.manager_ignored: Set[str] = set(['_parent']) - - @override - def __setattr__(self, name: str, value: Any) -> None: - if hasattr(self, 'manager_ignored') and name not in self.manager_ignored: - self.manager.add_element(name, value) - super().__setattr__(name, value) - - def _name_of_child(self, subelt: Any, context: Any, allow_unknown: bool = False) -> str: - self_name = self.manager.name_of(subelt) - if self_name is not None: - return self_name - else: - if allow_unknown: - return f"(unknown {subelt.__class__.__name__})" - else: - raise ValueError(f"no name for {subelt}") - - def _path_from(self, base: LibraryElement, allow_unknown: bool = False) -> List[str]: - if base is self: - return [] - else: - assert self._parent is not None, "can't get path / name for non-bound element" - return self._parent._path_from(base, allow_unknown) + [self._parent._name_of_child(self, base, allow_unknown)] - - def _name_from(self, base: LibraryElement, allow_unknown: bool = False) -> str: - """Returns the path name to (inclusive) this element from some starting point. - allow_unknown allows elements that haven't been assigned a name yet to not crash, - this is useful when called from an error so the _name_from error doesn't stomp the real error.""" - return '.'.join(self._path_from(base, allow_unknown)) - - @classmethod - def _static_def_name(cls) -> str: - """If this library element is defined by class (all instances have an equivalent library definition), - returns the definition name. Otherwise, should crash.""" - if cls.__module__ == "__main__": - # when the top-level design is run as main, the module name is __main__ which is meaningless - # and breaks when the HDL server tries to resolve the __main__ reference (to itself), - # so this needs to resolve the correct name - import inspect - import os - module = os.path.splitext(os.path.basename(inspect.getfile(cls)))[0] - else: - module = cls.__module__ - return module + "." + cls.__name__ - - def _get_def_name(self) -> str: - """Returns the definition name""" - return self.__class__._static_def_name() - - @abstractmethod - def _def_to_proto(self) -> Union[edgir.PortTypes, edgir.BlockLikeTypes]: ... + """Defines a library element, which optionally contains other library elements.""" + + _elt_properties: Dict[Tuple[Type[LibraryElement], EltPropertiesBase], Any] = {} + + @override + def __repr__(self) -> str: + return "%s@%02x" % (self._get_def_name(), (id(self) // 4) & 0xFF) + + def __init__(self) -> None: + self._parent: Optional[LibraryElement] = None # set by binding, None means not bound + + self.manager = SubElementManager() + self.manager_ignored: Set[str] = set(["_parent"]) + + @override + def __setattr__(self, name: str, value: Any) -> None: + if hasattr(self, "manager_ignored") and name not in self.manager_ignored: + self.manager.add_element(name, value) + super().__setattr__(name, value) + + def _name_of_child(self, subelt: Any, context: Any, allow_unknown: bool = False) -> str: + self_name = self.manager.name_of(subelt) + if self_name is not None: + return self_name + else: + if allow_unknown: + return f"(unknown {subelt.__class__.__name__})" + else: + raise ValueError(f"no name for {subelt}") + + def _path_from(self, base: LibraryElement, allow_unknown: bool = False) -> List[str]: + if base is self: + return [] + else: + assert self._parent is not None, "can't get path / name for non-bound element" + return self._parent._path_from(base, allow_unknown) + [ + self._parent._name_of_child(self, base, allow_unknown) + ] + + def _name_from(self, base: LibraryElement, allow_unknown: bool = False) -> str: + """Returns the path name to (inclusive) this element from some starting point. + allow_unknown allows elements that haven't been assigned a name yet to not crash, + this is useful when called from an error so the _name_from error doesn't stomp the real error.""" + return ".".join(self._path_from(base, allow_unknown)) + + @classmethod + def _static_def_name(cls) -> str: + """If this library element is defined by class (all instances have an equivalent library definition), + returns the definition name. Otherwise, should crash.""" + if cls.__module__ == "__main__": + # when the top-level design is run as main, the module name is __main__ which is meaningless + # and breaks when the HDL server tries to resolve the __main__ reference (to itself), + # so this needs to resolve the correct name + import inspect + import os + + module = os.path.splitext(os.path.basename(inspect.getfile(cls)))[0] + else: + module = cls.__module__ + return module + "." + cls.__name__ + + def _get_def_name(self) -> str: + """Returns the definition name""" + return self.__class__._static_def_name() + + @abstractmethod + def _def_to_proto(self) -> Union[edgir.PortTypes, edgir.BlockLikeTypes]: ... @non_library class HasMetadata(LibraryElement): - """A library element with the metadata dict-like field""" - def __init__(self) -> None: - super().__init__() - self._metadata: SubElementDict[Any] = self.manager.new_dict(Any) - - MetadataType = TypeVar('MetadataType', bound=Union[str, Mapping[str, Any], SubElementDict[Any], IdentityDict[Any, Any]]) - def Metadata(self, value: MetadataType) -> MetadataType: - """Adds a metadata field to this object. Reference to the value must not change, and reassignment will error. - Value may be changed until proto generation. - Utility method for library builders.""" - self._metadata.register(value) - return value - - BaseType = TypeVar('BaseType', bound='HasMetadata') - @classmethod - def _get_bases_of(cls, base_type: Type[BaseType]) -> Tuple[List[Type[BaseType]], List[Type[BaseType]]]: - """Returns all the base classes of this class, as a list of direct superclasses (including through non_library - elements) and a list of additional (indirect) superclasses. Direct superclasses are in MRO order, indirect - superclasses order is not defined (but MRO in current practice). - - mypy currently does not allow passing in abstract types, so generally calls to this need type: ignore.""" - direct_bases: Set[Type[HasMetadata]] = set() - def process_direct_base(bcls: Type[HasMetadata.BaseType]) -> None: - if not issubclass(bcls, base_type): - return # ignore above base_type - if (bcls, NonLibraryProperty) in bcls._elt_properties: # non-library, recurse into parents - for bcls_base in bcls.__bases__: - process_direct_base(bcls_base) - else: # anything else, directly append if not existing - direct_bases.add(bcls) - for bcls in cls.__bases__: - process_direct_base(bcls) - - ordered_direct_bases: List[Type[HasMetadata.BaseType]] = [] - ordered_indirect_bases: List[Type[HasMetadata.BaseType]] = [] - for mro_base in cls.__mro__[1:]: # ignore self - if mro_base in direct_bases: - ordered_direct_bases.append(mro_base) - elif issubclass(mro_base, base_type) and (mro_base, NonLibraryProperty) not in mro_base._elt_properties: - ordered_indirect_bases.append(mro_base) - - return ordered_direct_bases, ordered_indirect_bases - - def _populate_metadata(self, pb: edgir.Metadata, src: Any, - ref_map: Refable.RefMapType) -> None: - """Generate metadata from a given object.""" - if isinstance(src, str): - pb.text_leaf = src - elif isinstance(src, bytes): - pb.bin_leaf = src - elif isinstance(src, dict) or isinstance(src, SubElementDict) or isinstance(src, IdentityDict): - if isinstance(src, SubElementDict): # used at the top-level, for Metadata(...) - src.finalize() # TODO should this be here? - for key, val in src.items(): - assert isinstance(key, str), f'must overload _metadata_to_proto for non-str dict key {key}' - self._populate_metadata(pb.members.node[key], val, ref_map) - elif isinstance(src, list): - for idx, val in enumerate(src): - self._populate_metadata(pb.members.node[str(idx)], val, ref_map) - else: - raise ValueError(f'must overload _metadata_to_proto to handle unknown value {src}') + """A library element with the metadata dict-like field""" + + def __init__(self) -> None: + super().__init__() + self._metadata: SubElementDict[Any] = self.manager.new_dict(Any) + + MetadataType = TypeVar( + "MetadataType", bound=Union[str, Mapping[str, Any], SubElementDict[Any], IdentityDict[Any, Any]] + ) + + def Metadata(self, value: MetadataType) -> MetadataType: + """Adds a metadata field to this object. Reference to the value must not change, and reassignment will error. + Value may be changed until proto generation. + Utility method for library builders.""" + self._metadata.register(value) + return value + + BaseType = TypeVar("BaseType", bound="HasMetadata") + + @classmethod + def _get_bases_of(cls, base_type: Type[BaseType]) -> Tuple[List[Type[BaseType]], List[Type[BaseType]]]: + """Returns all the base classes of this class, as a list of direct superclasses (including through non_library + elements) and a list of additional (indirect) superclasses. Direct superclasses are in MRO order, indirect + superclasses order is not defined (but MRO in current practice). + + mypy currently does not allow passing in abstract types, so generally calls to this need type: ignore.""" + direct_bases: Set[Type[HasMetadata]] = set() + + def process_direct_base(bcls: Type[HasMetadata.BaseType]) -> None: + if not issubclass(bcls, base_type): + return # ignore above base_type + if (bcls, NonLibraryProperty) in bcls._elt_properties: # non-library, recurse into parents + for bcls_base in bcls.__bases__: + process_direct_base(bcls_base) + else: # anything else, directly append if not existing + direct_bases.add(bcls) + + for bcls in cls.__bases__: + process_direct_base(bcls) + + ordered_direct_bases: List[Type[HasMetadata.BaseType]] = [] + ordered_indirect_bases: List[Type[HasMetadata.BaseType]] = [] + for mro_base in cls.__mro__[1:]: # ignore self + if mro_base in direct_bases: + ordered_direct_bases.append(mro_base) + elif issubclass(mro_base, base_type) and (mro_base, NonLibraryProperty) not in mro_base._elt_properties: + ordered_indirect_bases.append(mro_base) + + return ordered_direct_bases, ordered_indirect_bases + + def _populate_metadata(self, pb: edgir.Metadata, src: Any, ref_map: Refable.RefMapType) -> None: + """Generate metadata from a given object.""" + if isinstance(src, str): + pb.text_leaf = src + elif isinstance(src, bytes): + pb.bin_leaf = src + elif isinstance(src, dict) or isinstance(src, SubElementDict) or isinstance(src, IdentityDict): + if isinstance(src, SubElementDict): # used at the top-level, for Metadata(...) + src.finalize() # TODO should this be here? + for key, val in src.items(): + assert isinstance(key, str), f"must overload _metadata_to_proto for non-str dict key {key}" + self._populate_metadata(pb.members.node[key], val, ref_map) + elif isinstance(src, list): + for idx, val in enumerate(src): + self._populate_metadata(pb.members.node[str(idx)], val, ref_map) + else: + raise ValueError(f"must overload _metadata_to_proto to handle unknown value {src}") diff --git a/edg/core/DesignTop.py b/edg/core/DesignTop.py index 26f6a0eb6..70de33375 100644 --- a/edg/core/DesignTop.py +++ b/edg/core/DesignTop.py @@ -11,182 +11,200 @@ from .IdentityDict import IdentityDict from .Blocks import BlockElaborationState from .HierarchyBlock import Block -from .MultipackBlock import MultipackBlock, PackedBlockAllocate, PackedBlockPortArray, PackedBlockParamArray, \ - PackedBlockParam +from .MultipackBlock import ( + MultipackBlock, + PackedBlockAllocate, + PackedBlockPortArray, + PackedBlockParamArray, + PackedBlockParam, +) from .Refinements import Refinements, DesignPath class DesignTop(Block): - """A top-level design, which may not have ports (including exports), but may define refinements. - """ - def __init__(self) -> None: - super().__init__() - self._packed_blocks = IdentityDict[ - Union[Block, PackedBlockAllocate], DesignPath]() # multipack part -> packed block (as path) - - @override - def Port(self, *args: Any, **kwargs: Any) -> Any: - raise ValueError("Can't create ports on design top") - - @override - def Export(self, *args: Any, **kwargs: Any) -> Any: - raise ValueError("Can't create ports on design top") - - def refinements(self) -> Refinements: - """Defines top-level refinements. - Subclasses should define refinements by stacking new refinements on a super().refinements() call.""" - # Include all the packing refinements as instance refinements - def make_packing_refinement(multipack_part: Union[Block, PackedBlockAllocate], path: DesignPath) -> \ - Tuple[DesignPath, Type[Block]]: - if isinstance(multipack_part, Block): - return path, type(multipack_part) - elif isinstance(multipack_part, PackedBlockAllocate): - assert multipack_part.parent._elt_sample is not None - return path, type(multipack_part.parent._elt_sample) - else: - raise TypeError - - return Refinements( - instance_refinements=[make_packing_refinement(multipack_part, path) - for multipack_part, path in self._packed_blocks.items()] - ) - - def multipack(self) -> None: - """Defines multipack packing rules, by defining multipack devices and providing packing connections. - Subclasses should define multipack by stacking on top of super().multipack().""" - pass - - # TODO make this non-overriding? - this needs to call multipack after contents - @override - def _elaborated_def_to_proto(self) -> edgir.HierarchyBlock: - prev_element = builder.push_element(self) - assert prev_element is None - try: - assert self._elaboration_state == BlockElaborationState.init - self._elaboration_state = BlockElaborationState.contents - self.contents() - self.multipack() - self._elaboration_state = BlockElaborationState.post_contents - finally: - builder.pop_to(prev_element) - return self._def_to_proto() - - @override - def _populate_def_proto_block_contents(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: - """Add multipack constraints""" - super()._populate_def_proto_block_contents(pb, ref_map) - - # Since ConstraintExpr arrays don't have the allocate construct (like connects), - # we need to aggregate them into a packed array format (instead of generating a constraint for each element) - # constr name -> (assign dst, assign src elt) - packed_params: Dict[str, Tuple[edgir.LocalPath, List[edgir.LocalPath]]] = {} - - for multipack_part, packed_path in self._packed_blocks.items(): - if isinstance(multipack_part, Block): - multipack_block = multipack_part._parent - multipack_part_block = multipack_part - elif isinstance(multipack_part, PackedBlockAllocate): - multipack_block = multipack_part.parent._parent - assert isinstance(multipack_part.parent._elt_sample, Block) # may be optional - multipack_part_block = multipack_part.parent._elt_sample - else: - raise TypeError - assert isinstance(multipack_block, MultipackBlock) - multipack_name = self._name_of_child(multipack_block, self) - multipack_ref_base = edgir.LocalPath() - multipack_ref_base.steps.add().name = multipack_name - multipack_ref_map = multipack_block._create_ref_map(multipack_ref_base) - - packing_rule = multipack_block._get_block_packing_rule(multipack_part) - packed_ref_base = edgir.LocalPath() - for packed_path_part in packed_path: - packed_ref_base.steps.add().name = packed_path_part - packed_ref_map = multipack_part_block._create_ref_map(packed_ref_base) - - if isinstance(multipack_part, Block): - part_name = multipack_block._name_of_child(multipack_part, self) - elif isinstance(multipack_part, PackedBlockAllocate): - part_name = multipack_block._name_of_child(multipack_part.parent, self) - assert multipack_part.suggested_name, "multipack parts must have suggested name, for consistency" - part_name += f"[{multipack_part.suggested_name}]" - else: - raise TypeError - - for exterior_port, packed_port in packing_rule.tunnel_exports.items(): - if isinstance(packed_port, Port): - packed_port_port = packed_port - elif isinstance(packed_port, PackedBlockPortArray): - packed_port_port = packed_port.port + """A top-level design, which may not have ports (including exports), but may define refinements.""" + + def __init__(self) -> None: + super().__init__() + self._packed_blocks = IdentityDict[ + Union[Block, PackedBlockAllocate], DesignPath + ]() # multipack part -> packed block (as path) + + @override + def Port(self, *args: Any, **kwargs: Any) -> Any: + raise ValueError("Can't create ports on design top") + + @override + def Export(self, *args: Any, **kwargs: Any) -> Any: + raise ValueError("Can't create ports on design top") + + def refinements(self) -> Refinements: + """Defines top-level refinements. + Subclasses should define refinements by stacking new refinements on a super().refinements() call.""" + + # Include all the packing refinements as instance refinements + def make_packing_refinement( + multipack_part: Union[Block, PackedBlockAllocate], path: DesignPath + ) -> Tuple[DesignPath, Type[Block]]: + if isinstance(multipack_part, Block): + return path, type(multipack_part) + elif isinstance(multipack_part, PackedBlockAllocate): + assert multipack_part.parent._elt_sample is not None + return path, type(multipack_part.parent._elt_sample) + else: + raise TypeError + + return Refinements( + instance_refinements=[ + make_packing_refinement(multipack_part, path) for multipack_part, path in self._packed_blocks.items() + ] + ) + + def multipack(self) -> None: + """Defines multipack packing rules, by defining multipack devices and providing packing connections. + Subclasses should define multipack by stacking on top of super().multipack().""" + pass + + # TODO make this non-overriding? - this needs to call multipack after contents + @override + def _elaborated_def_to_proto(self) -> edgir.HierarchyBlock: + prev_element = builder.push_element(self) + assert prev_element is None + try: + assert self._elaboration_state == BlockElaborationState.init + self._elaboration_state = BlockElaborationState.contents + self.contents() + self.multipack() + self._elaboration_state = BlockElaborationState.post_contents + finally: + builder.pop_to(prev_element) + return self._def_to_proto() + + @override + def _populate_def_proto_block_contents(self, pb: edgir.BlockLikeTypes, ref_map: Refable.RefMapType) -> None: + """Add multipack constraints""" + super()._populate_def_proto_block_contents(pb, ref_map) + + # Since ConstraintExpr arrays don't have the allocate construct (like connects), + # we need to aggregate them into a packed array format (instead of generating a constraint for each element) + # constr name -> (assign dst, assign src elt) + packed_params: Dict[str, Tuple[edgir.LocalPath, List[edgir.LocalPath]]] = {} + + for multipack_part, packed_path in self._packed_blocks.items(): + if isinstance(multipack_part, Block): + multipack_block = multipack_part._parent + multipack_part_block = multipack_part + elif isinstance(multipack_part, PackedBlockAllocate): + multipack_block = multipack_part.parent._parent + assert isinstance(multipack_part.parent._elt_sample, Block) # may be optional + multipack_part_block = multipack_part.parent._elt_sample + else: + raise TypeError + assert isinstance(multipack_block, MultipackBlock) + multipack_name = self._name_of_child(multipack_block, self) + multipack_ref_base = edgir.LocalPath() + multipack_ref_base.steps.add().name = multipack_name + multipack_ref_map = multipack_block._create_ref_map(multipack_ref_base) + + packing_rule = multipack_block._get_block_packing_rule(multipack_part) + packed_ref_base = edgir.LocalPath() + for packed_path_part in packed_path: + packed_ref_base.steps.add().name = packed_path_part + packed_ref_map = multipack_part_block._create_ref_map(packed_ref_base) + + if isinstance(multipack_part, Block): + part_name = multipack_block._name_of_child(multipack_part, self) + elif isinstance(multipack_part, PackedBlockAllocate): + part_name = multipack_block._name_of_child(multipack_part.parent, self) + assert multipack_part.suggested_name, "multipack parts must have suggested name, for consistency" + part_name += f"[{multipack_part.suggested_name}]" + else: + raise TypeError + + for exterior_port, packed_port in packing_rule.tunnel_exports.items(): + if isinstance(packed_port, Port): + packed_port_port = packed_port + elif isinstance(packed_port, PackedBlockPortArray): + packed_port_port = packed_port.port + else: + raise TypeError + packed_port_name = multipack_part_block._name_of_child(packed_port_port, self) + exported_tunnel = edgir.add_pair( + pb.constraints, f"(packed){multipack_name}.{part_name}.{packed_port_name}" + ).exportedTunnel + exported_tunnel.internal_block_port.ref.CopyFrom(multipack_ref_map[exterior_port]) + if isinstance(packed_port, PackedBlockPortArray): + assert isinstance(multipack_part, PackedBlockAllocate) + assert multipack_part.suggested_name + exported_tunnel.internal_block_port.ref.steps.add().allocate = multipack_part.suggested_name + exported_tunnel.exterior_port.ref.CopyFrom(packed_ref_map[packed_port_port]) + + for multipack_param, packed_param in packing_rule.tunnel_assigns.items(): + if isinstance(packed_param, ConstraintExpr): + packed_param_name = multipack_part_block._name_of_child(packed_param, self) + assign_tunnel = edgir.add_pair( + pb.constraints, f"(packed){multipack_name}.{part_name}.{packed_param_name}" + ).assignTunnel + assign_tunnel.dst.CopyFrom(multipack_ref_map[multipack_param]) + assign_tunnel.src.ref.CopyFrom(packed_ref_map[packed_param]) + elif isinstance(packed_param, PackedBlockParamArray): + multipack_param_name = multipack_block._name_of_child(multipack_param, self) + constr_name = f"(packed){multipack_name}.{multipack_param_name}" + packed_params.setdefault(constr_name, (multipack_ref_map[multipack_param], []))[1].append( + packed_ref_map[packed_param.param] + ) + else: + raise TypeError + + for multipack_param, unpacked_param in packing_rule.tunnel_unpack_assigns.items(): + if isinstance(unpacked_param, ConstraintExpr): + multipack_param_name = multipack_block._name_of_child(multipack_param, self) + # TODO need better constraint naming scheme + assign_tunnel = edgir.add_pair( + pb.constraints, f"(unpacked){multipack_name}.{part_name}.{multipack_param_name}" + ).assignTunnel + assign_tunnel.dst.CopyFrom(packed_ref_map[unpacked_param]) + assign_tunnel.src.ref.CopyFrom(multipack_ref_map[multipack_param]) + elif isinstance(unpacked_param, PackedBlockParam): + multipack_param_name = multipack_block._name_of_child(multipack_param, self) + # TODO need better constraint naming scheme + assign_tunnel = edgir.add_pair( + pb.constraints, f"(unpacked){multipack_name}.{part_name}.{multipack_param_name}" + ).assignTunnel + assign_tunnel.dst.CopyFrom(packed_ref_map[unpacked_param.param]) + assign_tunnel.src.ref.CopyFrom(multipack_ref_map[multipack_param]) + else: + raise TypeError + + # Generate packed array assigns (see comment near top of function) + for constr_name, (assign_dst, assign_srcs) in packed_params.items(): + assign_tunnel = edgir.add_pair(pb.constraints, constr_name).assignTunnel + assign_tunnel.dst.CopyFrom(assign_dst) + assign_src_vals = assign_tunnel.src.array.vals + for assign_src in assign_srcs: + assign_src_vals.add().ref.CopyFrom(assign_src) + + PackedBlockType = TypeVar("PackedBlockType", bound=MultipackBlock) + + def PackedBlock(self, tpe: PackedBlockType) -> PackedBlockType: + """Instantiates a multipack block, that can be used to pack constituent blocks arbitrarily deep in the design.""" + # TODO: additional checks and enforcement beyond what Block provides - eg disallowing .connect operations + return self.Block(tpe) + + def pack(self, multipack_part: Union[Block, PackedBlockAllocate], path: DesignPath) -> None: + """Packs a block (arbitrarily deep in the design tree, specified as a path) into a PackedBlock multipack block.""" + if self._elaboration_state not in [ + BlockElaborationState.init, + BlockElaborationState.contents, + BlockElaborationState.generate, + ]: + raise BlockDefinitionError(type(self), "can only define multipack in init, contents, or generate") + if isinstance(multipack_part, Block): + multipack_block = multipack_part._parent + elif isinstance(multipack_part, PackedBlockAllocate): + multipack_block = multipack_part.parent._parent else: - raise TypeError - packed_port_name = multipack_part_block._name_of_child(packed_port_port, self) - exported_tunnel = edgir.add_pair(pb.constraints, - f"(packed){multipack_name}.{part_name}.{packed_port_name}").exportedTunnel - exported_tunnel.internal_block_port.ref.CopyFrom(multipack_ref_map[exterior_port]) - if isinstance(packed_port, PackedBlockPortArray): - assert isinstance(multipack_part, PackedBlockAllocate) - assert multipack_part.suggested_name - exported_tunnel.internal_block_port.ref.steps.add().allocate = multipack_part.suggested_name - exported_tunnel.exterior_port.ref.CopyFrom(packed_ref_map[packed_port_port]) - - for multipack_param, packed_param in packing_rule.tunnel_assigns.items(): - if isinstance(packed_param, ConstraintExpr): - packed_param_name = multipack_part_block._name_of_child(packed_param, self) - assign_tunnel = edgir.add_pair(pb.constraints, - f"(packed){multipack_name}.{part_name}.{packed_param_name}").assignTunnel - assign_tunnel.dst.CopyFrom(multipack_ref_map[multipack_param]) - assign_tunnel.src.ref.CopyFrom(packed_ref_map[packed_param]) - elif isinstance(packed_param, PackedBlockParamArray): - multipack_param_name = multipack_block._name_of_child(multipack_param, self) - constr_name = f"(packed){multipack_name}.{multipack_param_name}" - packed_params.setdefault(constr_name, (multipack_ref_map[multipack_param], []))[1].append( - packed_ref_map[packed_param.param]) - else: - raise TypeError - - for multipack_param, unpacked_param in packing_rule.tunnel_unpack_assigns.items(): - if isinstance(unpacked_param, ConstraintExpr): - multipack_param_name = multipack_block._name_of_child(multipack_param, self) - # TODO need better constraint naming scheme - assign_tunnel = edgir.add_pair(pb.constraints, - f"(unpacked){multipack_name}.{part_name}.{multipack_param_name}").assignTunnel - assign_tunnel.dst.CopyFrom(packed_ref_map[unpacked_param]) - assign_tunnel.src.ref.CopyFrom(multipack_ref_map[multipack_param]) - elif isinstance(unpacked_param, PackedBlockParam): - multipack_param_name = multipack_block._name_of_child(multipack_param, self) - # TODO need better constraint naming scheme - assign_tunnel = edgir.add_pair(pb.constraints, - f"(unpacked){multipack_name}.{part_name}.{multipack_param_name}").assignTunnel - assign_tunnel.dst.CopyFrom(packed_ref_map[unpacked_param.param]) - assign_tunnel.src.ref.CopyFrom(multipack_ref_map[multipack_param]) - else: - raise TypeError - - # Generate packed array assigns (see comment near top of function) - for constr_name, (assign_dst, assign_srcs) in packed_params.items(): - assign_tunnel = edgir.add_pair(pb.constraints, constr_name).assignTunnel - assign_tunnel.dst.CopyFrom(assign_dst) - assign_src_vals = assign_tunnel.src.array.vals - for assign_src in assign_srcs: - assign_src_vals.add().ref.CopyFrom(assign_src) - - PackedBlockType = TypeVar('PackedBlockType', bound=MultipackBlock) - def PackedBlock(self, tpe: PackedBlockType) -> PackedBlockType: - """Instantiates a multipack block, that can be used to pack constituent blocks arbitrarily deep in the design.""" - # TODO: additional checks and enforcement beyond what Block provides - eg disallowing .connect operations - return self.Block(tpe) - - def pack(self, multipack_part: Union[Block, PackedBlockAllocate], path: DesignPath) -> None: - """Packs a block (arbitrarily deep in the design tree, specified as a path) into a PackedBlock multipack block.""" - if self._elaboration_state not in \ - [BlockElaborationState.init, BlockElaborationState.contents, BlockElaborationState.generate]: - raise BlockDefinitionError(type(self), "can only define multipack in init, contents, or generate") - if isinstance(multipack_part, Block): - multipack_block = multipack_part._parent - elif isinstance(multipack_part, PackedBlockAllocate): - multipack_block = multipack_part.parent._parent - else: - raise TypeError - assert isinstance(multipack_block, MultipackBlock), "block must be a part of a MultipackBlock" - assert self._blocks.name_of(multipack_block), "containing MultipackBlock must be a PackedBlock" - self._packed_blocks[multipack_part] = path + raise TypeError + assert isinstance(multipack_block, MultipackBlock), "block must be a part of a MultipackBlock" + assert self._blocks.name_of(multipack_block), "containing MultipackBlock must be a PackedBlock" + self._packed_blocks[multipack_part] = path diff --git a/edg/core/Generator.py b/edg/core/Generator.py index ceefa8aaa..dff817991 100644 --- a/edg/core/Generator.py +++ b/edg/core/Generator.py @@ -18,160 +18,181 @@ from .HierarchyBlock import Block -CastableType = TypeVar('CastableType', bound=Any) +CastableType = TypeVar("CastableType", bound=Any) + + @non_library class GeneratorBlock(Block): - """Block which allows arbitrary Python code to generate its internal subcircuit, - and unlike regular Blocks can rely on Python values of solved parameters. - """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._generator: Optional[GeneratorBlock.GeneratorRecord] = None - self._generator_params_list: list[ConstraintExpr] = [] - self._generator_param_values = IdentityDict[ConstraintExpr, Any]() - - def generator_param(self, *params: ConstraintExpr) -> None: - """Declares some parameter to be a generator, so in generate() it can be used in self.get(). - Parameters that have not been called in generator_param will error out if used in self.get().""" - if self._elaboration_state not in (BlockElaborationState.init, BlockElaborationState.contents): - raise BlockDefinitionError(type(self), "can't call generator_param(...) outside __init__ or contents", - "call generator_param(...) inside __init__ or contents only, and remember to call super().__init__()") - for param in params: - if not isinstance(param, ConstraintExpr): - raise EdgTypeError(f"generator_param(...) param", param, ConstraintExpr) - if param.binding is None: - raise BlockDefinitionError(type(self), "generator_param(...) param must be bound") - if not isinstance(param.binding, (InitParamBinding, AllocatedBinding, IsConnectedBinding)): - raise BlockDefinitionError(type(self), "generator_param(...) param must be an __init__ param, port requested, or port is_connected") - - self._generator_params_list.append(param) - - WrappedType = TypeVar('WrappedType', bound=Any) - def get(self, param: ConstraintExpr[WrappedType, Any]) -> WrappedType: - return self._generator_param_values[param] # type: ignore - - # Generator dependency data - # - class GeneratorRecord(NamedTuple): - fn: Callable[..., None] - req_params: Tuple[ConstraintExpr, ...] # all required params for generator to fire - fn_args: Tuple[ConstraintExpr, ...] # params to unpack for the generator function - - @deprecated(reason="implement self.generate() instead (using self.get(...), self.generator_param(...))") - def generator(self, fn: Callable[..., None], *reqs: Any) -> None: + """Block which allows arbitrary Python code to generate its internal subcircuit, + and unlike regular Blocks can rely on Python values of solved parameters. """ - Registers a generator function - :param fn: function (of self) to invoke, where the parameter list lines up with reqs - :param reqs: required parameters, the value of which are passed to the generator function - Note, generator parameters must be __init__ parameters because the block is not traversed before generation, - and any internal constraints (like parameter assignments from within) are not evaluated. - """ - assert callable(fn), f"fn {fn} must be a method (callable)" - assert self._generator is None, f"redefinition of generator, multiple generators not allowed" - - for (i, req_param) in enumerate(reqs): - assert isinstance(req_param.binding, InitParamBinding) or \ - (isinstance(req_param.binding, (AllocatedBinding, IsConnectedBinding)) - and req_param.binding.src._parent is self), \ - f"generator parameter {i} {req_param} not an __init__ parameter" - self._generator = GeneratorBlock.GeneratorRecord(fn, reqs, reqs) - - def generate(self) -> None: - """Generate function which has access to the value of generator params. Implement me.""" - pass - - # Generator serialization and parsing - # - @override - def _def_to_proto(self) -> edgir.HierarchyBlock: - if self._elaboration_state != BlockElaborationState.post_generate: # only write generator on the stub definition - ref_map = self._create_ref_map() - pb = edgir.HierarchyBlock() - self._populate_def_proto_block_base(pb) - pb.generator.SetInParent() # even if rest of the fields are empty, make sure to create a record - - if type(self).generate is not GeneratorBlock.generate: - assert self._generator is None, "new-style generator may not define self.generator(...)" - for param in self._generator_params_list: - pb.generator.required_params.add().CopyFrom(ref_map[param]) - elif self._generator is not None: # legacy generator style - assert len(self._generator_params_list) == 0, "legacy self.generator(...) must not have generator_params()" - for req_param in self._generator.req_params: - pb.generator.required_params.add().CopyFrom(ref_map[req_param]) - elif (self.__class__, AbstractBlockProperty) in self._elt_properties: - pass # abstract blocks allowed to not define a generator - else: - raise BlockDefinitionError(type(self), "Generator missing generate implementation", "define generate") - return pb - else: - return super()._def_to_proto() - - def _generated_def_to_proto(self, generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]]) -> \ - edgir.HierarchyBlock: - prev_element = builder.push_element(self) - assert prev_element is None - - try: - assert self._elaboration_state == BlockElaborationState.init - self._elaboration_state = BlockElaborationState.contents - self.contents() - self._elaboration_state = BlockElaborationState.generate - - # Translate parameter values to function arguments - ref_map = self._create_ref_map() - generate_values_map = {path.SerializeToString(): value for (path, value) in generate_values} - - assert (self.__class__, AbstractBlockProperty) not in self._elt_properties # abstract blocks can't generate - if type(self).generate is not GeneratorBlock.generate: - for param in self._generator_params_list: - self._generator_param_values[param] = param._from_lit(generate_values_map[ref_map[param].SerializeToString()]) - self.generate() - elif self._generator is not None: # legacy generator style - fn_args = [arg_param._from_lit(generate_values_map[ref_map[arg_param].SerializeToString()]) - for arg_param in self._generator.fn_args] - self._generator.fn(*fn_args) - else: - raise BlockDefinitionError(type(self), "Generator missing generate implementation", "define generate") - - self._elaboration_state = BlockElaborationState.post_generate - finally: - builder.pop_to(prev_element) - - return self._def_to_proto() + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._generator: Optional[GeneratorBlock.GeneratorRecord] = None + self._generator_params_list: list[ConstraintExpr] = [] + self._generator_param_values = IdentityDict[ConstraintExpr, Any]() + + def generator_param(self, *params: ConstraintExpr) -> None: + """Declares some parameter to be a generator, so in generate() it can be used in self.get(). + Parameters that have not been called in generator_param will error out if used in self.get().""" + if self._elaboration_state not in (BlockElaborationState.init, BlockElaborationState.contents): + raise BlockDefinitionError( + type(self), + "can't call generator_param(...) outside __init__ or contents", + "call generator_param(...) inside __init__ or contents only, and remember to call super().__init__()", + ) + for param in params: + if not isinstance(param, ConstraintExpr): + raise EdgTypeError(f"generator_param(...) param", param, ConstraintExpr) + if param.binding is None: + raise BlockDefinitionError(type(self), "generator_param(...) param must be bound") + if not isinstance(param.binding, (InitParamBinding, AllocatedBinding, IsConnectedBinding)): + raise BlockDefinitionError( + type(self), + "generator_param(...) param must be an __init__ param, port requested, or port is_connected", + ) + + self._generator_params_list.append(param) + + WrappedType = TypeVar("WrappedType", bound=Any) + + def get(self, param: ConstraintExpr[WrappedType, Any]) -> WrappedType: + return self._generator_param_values[param] # type: ignore + + # Generator dependency data + # + class GeneratorRecord(NamedTuple): + fn: Callable[..., None] + req_params: Tuple[ConstraintExpr, ...] # all required params for generator to fire + fn_args: Tuple[ConstraintExpr, ...] # params to unpack for the generator function + + @deprecated(reason="implement self.generate() instead (using self.get(...), self.generator_param(...))") + def generator(self, fn: Callable[..., None], *reqs: Any) -> None: + """ + Registers a generator function + :param fn: function (of self) to invoke, where the parameter list lines up with reqs + :param reqs: required parameters, the value of which are passed to the generator function + + Note, generator parameters must be __init__ parameters because the block is not traversed before generation, + and any internal constraints (like parameter assignments from within) are not evaluated. + """ + assert callable(fn), f"fn {fn} must be a method (callable)" + assert self._generator is None, f"redefinition of generator, multiple generators not allowed" + + for i, req_param in enumerate(reqs): + assert isinstance(req_param.binding, InitParamBinding) or ( + isinstance(req_param.binding, (AllocatedBinding, IsConnectedBinding)) + and req_param.binding.src._parent is self + ), f"generator parameter {i} {req_param} not an __init__ parameter" + self._generator = GeneratorBlock.GeneratorRecord(fn, reqs, reqs) + + def generate(self) -> None: + """Generate function which has access to the value of generator params. Implement me.""" + pass + + # Generator serialization and parsing + # + @override + def _def_to_proto(self) -> edgir.HierarchyBlock: + if ( + self._elaboration_state != BlockElaborationState.post_generate + ): # only write generator on the stub definition + ref_map = self._create_ref_map() + pb = edgir.HierarchyBlock() + self._populate_def_proto_block_base(pb) + pb.generator.SetInParent() # even if rest of the fields are empty, make sure to create a record + + if type(self).generate is not GeneratorBlock.generate: + assert self._generator is None, "new-style generator may not define self.generator(...)" + for param in self._generator_params_list: + pb.generator.required_params.add().CopyFrom(ref_map[param]) + elif self._generator is not None: # legacy generator style + assert ( + len(self._generator_params_list) == 0 + ), "legacy self.generator(...) must not have generator_params()" + for req_param in self._generator.req_params: + pb.generator.required_params.add().CopyFrom(ref_map[req_param]) + elif (self.__class__, AbstractBlockProperty) in self._elt_properties: + pass # abstract blocks allowed to not define a generator + else: + raise BlockDefinitionError(type(self), "Generator missing generate implementation", "define generate") + return pb + else: + return super()._def_to_proto() + + def _generated_def_to_proto( + self, generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]] + ) -> edgir.HierarchyBlock: + prev_element = builder.push_element(self) + assert prev_element is None + + try: + assert self._elaboration_state == BlockElaborationState.init + self._elaboration_state = BlockElaborationState.contents + self.contents() + self._elaboration_state = BlockElaborationState.generate + + # Translate parameter values to function arguments + ref_map = self._create_ref_map() + generate_values_map = {path.SerializeToString(): value for (path, value) in generate_values} + + assert (self.__class__, AbstractBlockProperty) not in self._elt_properties # abstract blocks can't generate + if type(self).generate is not GeneratorBlock.generate: + for param in self._generator_params_list: + self._generator_param_values[param] = param._from_lit( + generate_values_map[ref_map[param].SerializeToString()] + ) + self.generate() + elif self._generator is not None: # legacy generator style + fn_args = [ + arg_param._from_lit(generate_values_map[ref_map[arg_param].SerializeToString()]) + for arg_param in self._generator.fn_args + ] + self._generator.fn(*fn_args) + else: + raise BlockDefinitionError(type(self), "Generator missing generate implementation", "define generate") + + self._elaboration_state = BlockElaborationState.post_generate + finally: + builder.pop_to(prev_element) + + return self._def_to_proto() @non_library class DefaultExportBlock(GeneratorBlock): - """EXPERIMENTAL UTILITY CLASS. There needs to be a cleaner way to address this eventually, - perhaps as a core compiler construct. - This encapsulates the common pattern of an optional export, which if not externally connected, - connects the internal port to some other default port. - TODO The default can be specified as a port, or a function that returns a port (e.g. to instantiate adapters).""" - def __init__(self) -> None: - super().__init__() - self._default_exports: List[Tuple[BasePort, Port, Port]] = [] # internal, exported, default - - ExportType = TypeVar('ExportType', bound=BasePort) - @override - def Export(self, port: ExportType, *args: Any, default: Optional[Port] = None, **kwargs: Any) -> ExportType: - """A generator-only variant of Export that supports an optional default (either internal or external) - to connect the (internal) port being exported to, if the external exported port is not connected.""" - if default is None: - new_port = super().Export(port, *args, **kwargs) - else: - assert 'optional' not in kwargs, "optional must not be specified with default" - new_port = super().Export(port, *args, optional=True, _connect=False, **kwargs) - assert isinstance(new_port, Port), "defaults only supported with Port types" - self.generator_param(new_port.is_connected()) - self._default_exports.append((port, new_port, default)) - return new_port - - @override - def generate(self) -> None: - super().generate() - for (internal, exported, default) in self._default_exports: - if self.get(exported.is_connected()): - self.connect(internal, exported) - else: - self.connect(internal, default) + """EXPERIMENTAL UTILITY CLASS. There needs to be a cleaner way to address this eventually, + perhaps as a core compiler construct. + This encapsulates the common pattern of an optional export, which if not externally connected, + connects the internal port to some other default port. + TODO The default can be specified as a port, or a function that returns a port (e.g. to instantiate adapters).""" + + def __init__(self) -> None: + super().__init__() + self._default_exports: List[Tuple[BasePort, Port, Port]] = [] # internal, exported, default + + ExportType = TypeVar("ExportType", bound=BasePort) + + @override + def Export(self, port: ExportType, *args: Any, default: Optional[Port] = None, **kwargs: Any) -> ExportType: + """A generator-only variant of Export that supports an optional default (either internal or external) + to connect the (internal) port being exported to, if the external exported port is not connected.""" + if default is None: + new_port = super().Export(port, *args, **kwargs) + else: + assert "optional" not in kwargs, "optional must not be specified with default" + new_port = super().Export(port, *args, optional=True, _connect=False, **kwargs) + assert isinstance(new_port, Port), "defaults only supported with Port types" + self.generator_param(new_port.is_connected()) + self._default_exports.append((port, new_port, default)) + return new_port + + @override + def generate(self) -> None: + super().generate() + for internal, exported, default in self._default_exports: + if self.get(exported.is_connected()): + self.connect(internal, exported) + else: + self.connect(internal, default) diff --git a/edg/core/HdlUserExceptions.py b/edg/core/HdlUserExceptions.py index 5e42f6df5..b8ca0983e 100644 --- a/edg/core/HdlUserExceptions.py +++ b/edg/core/HdlUserExceptions.py @@ -1,53 +1,59 @@ from typing import Any, Type, Union, Tuple, TYPE_CHECKING if TYPE_CHECKING: - from edg import BaseBlock + from edg import BaseBlock class EdslUserError(Exception): - """Base exception class where user error in writing EDSL is the likely cause.""" - def __init__(self, exc: str, resolution: str = ""): - super().__init__(exc) + """Base exception class where user error in writing EDSL is the likely cause.""" + + def __init__(self, exc: str, resolution: str = ""): + super().__init__(exc) class EdgTypeError(EdslUserError): - """Argument of the wrong type passed into a EDG core function.""" - def __init__(self, item_desc: str, object: Any, expected_type: Union[Type[Any], Tuple[Type[Any], ...]]): - if isinstance(expected_type, tuple): - expected_type_str = '/'.join([t.__name__ for t in expected_type]) - else: - expected_type_str = expected_type.__name__ + """Argument of the wrong type passed into a EDG core function.""" + + def __init__(self, item_desc: str, object: Any, expected_type: Union[Type[Any], Tuple[Type[Any], ...]]): + if isinstance(expected_type, tuple): + expected_type_str = "/".join([t.__name__ for t in expected_type]) + else: + expected_type_str = expected_type.__name__ - super().__init__(f"{item_desc} expected to be of type {expected_type_str}, got {object} of type {type(object).__name__}", - f"ensure {item_desc} is of type {expected_type_str}") + super().__init__( + f"{item_desc} expected to be of type {expected_type_str}, got {object} of type {type(object).__name__}", + f"ensure {item_desc} is of type {expected_type_str}", + ) class EdgContextError(EdslUserError): - """A context, like implicit scope, is used incorrectly""" - pass + """A context, like implicit scope, is used incorrectly""" + + pass class ConnectError(Exception): - """Base class for all errors that may occur during a connect statement""" + """Base class for all errors that may occur during a connect statement""" class UnconnectableError(Exception): - """When a link cannot be inferred""" + """When a link cannot be inferred""" class UnconnectedRequiredPortError(Exception): - """When required ports in children are not connected during proto gen""" + """When required ports in children are not connected during proto gen""" class UnreachableParameterError(Exception): - """When a parameter is referenced that can't be reached""" + """When a parameter is referenced that can't be reached""" class BlockDefinitionError(EdslUserError): - """Base error for likely mistakes when writing a block definition""" - def __init__(self, block_type: Type['BaseBlock'], exc: str, resolution: str = '') -> None: - super().__init__(f"invalid block definition for {block_type}: {exc}", resolution) + """Base error for likely mistakes when writing a block definition""" + + def __init__(self, block_type: Type["BaseBlock"], exc: str, resolution: str = "") -> None: + super().__init__(f"invalid block definition for {block_type}: {exc}", resolution) class ChainError(BlockDefinitionError): - """Base error for bad elements in a chain connect""" + """Base error for bad elements in a chain connect""" diff --git a/edg/core/HierarchyBlock.py b/edg/core/HierarchyBlock.py index 15a3f40ba..9b5834e1f 100644 --- a/edg/core/HierarchyBlock.py +++ b/edg/core/HierarchyBlock.py @@ -10,8 +10,18 @@ from .. import edgir from .Builder import builder -from . import ArrayStringExpr, ArrayRangeExpr, ArrayFloatExpr, ArrayIntExpr, ArrayBoolExpr, ArrayBoolLike, ArrayIntLike, \ - ArrayFloatLike, ArrayRangeLike, ArrayStringLike +from . import ( + ArrayStringExpr, + ArrayRangeExpr, + ArrayFloatExpr, + ArrayIntExpr, + ArrayBoolExpr, + ArrayBoolLike, + ArrayIntLike, + ArrayFloatLike, + ArrayRangeLike, + ArrayStringLike, +) from .Array import BaseVector, Vector from .Binding import InitParamBinding, AssignBinding from .Blocks import BaseBlock, Connection, BlockElaborationState, AbstractBlockProperty, BaseBlockMeta @@ -25,660 +35,745 @@ from .Ports import BasePort, Port if TYPE_CHECKING: - from .BlockInterfaceMixin import BlockInterfaceMixin + from .BlockInterfaceMixin import BlockInterfaceMixin def init_in_parent(fn: Any) -> Any: - warnings.warn( - f"in {fn}, @init_in_parent is no longer needed, the annotation can be removed without replacement", - DeprecationWarning, - stacklevel=2 - ) - - @functools.wraps(fn) - def wrapped(self: Block, *args: Any, **kwargs: Any) -> Any: - # in concept, the outer deprecation should fire, but it doesn't consistently, so this is added for redundancy warnings.warn( - f"in {fn}, @init_in_parent is no longer needed, the annotation can be removed without replacement", - DeprecationWarning + f"in {fn}, @init_in_parent is no longer needed, the annotation can be removed without replacement", + DeprecationWarning, + stacklevel=2, ) - return fn(self, *args, **kwargs) - return wrapped + + @functools.wraps(fn) + def wrapped(self: Block, *args: Any, **kwargs: Any) -> Any: + # in concept, the outer deprecation should fire, but it doesn't consistently, so this is added for redundancy + warnings.warn( + f"in {fn}, @init_in_parent is no longer needed, the annotation can be removed without replacement", + DeprecationWarning, + ) + return fn(self, *args, **kwargs) + + return wrapped # TODO not statically type checked, since the port may be externally facing. TODO: maybe PortTags should be bridgeable? class ImplicitConnect: - """Implicit connect definition. a port and all the implicit connect tags it supports. - To implicitly connect to a sub-block's port, this tags list must be a superset of the sub-block port's tag list.""" - def __init__(self, port: Union[Port, Connection], tags: List[PortTag]) -> None: - self.port = port - self.tags = set(tags) + """Implicit connect definition. a port and all the implicit connect tags it supports. + To implicitly connect to a sub-block's port, this tags list must be a superset of the sub-block port's tag list.""" + + def __init__(self, port: Union[Port, Connection], tags: List[PortTag]) -> None: + self.port = port + self.tags = set(tags) class ImplicitScope: - def __init__(self, parent: Block, implicits: Iterable[ImplicitConnect]) -> None: - self.open: Optional[bool] = None - self.parent = parent - self.implicits = tuple(implicits) + def __init__(self, parent: Block, implicits: Iterable[ImplicitConnect]) -> None: + self.open: Optional[bool] = None + self.parent = parent + self.implicits = tuple(implicits) + + BlockType = TypeVar("BlockType", bound="Block") - BlockType = TypeVar('BlockType', bound='Block') - def Block(self, tpe: BlockType) -> BlockType: - """Instantiates a block and performs implicit connections.""" - if self.open is None: - raise EdgContextError("can only use ImplicitScope.Block(...) in a Python with context") - elif self.open == False: - raise EdgContextError("can't use ImplicitScope.Block(...) after a Python with context") + def Block(self, tpe: BlockType) -> BlockType: + """Instantiates a block and performs implicit connections.""" + if self.open is None: + raise EdgContextError("can only use ImplicitScope.Block(...) in a Python with context") + elif self.open == False: + raise EdgContextError("can't use ImplicitScope.Block(...) after a Python with context") - block = self.parent.Block(tpe) + block = self.parent.Block(tpe) - already_connected_ports = IdentitySet[BasePort]() - for implicit in self.implicits: - # for all ports, only the first connectable implicit should connect - for tag in implicit.tags: - for port in block._get_ports_by_tag({tag}): - if port not in already_connected_ports: - self.parent.connect(implicit.port, port) - already_connected_ports.add(port) + already_connected_ports = IdentitySet[BasePort]() + for implicit in self.implicits: + # for all ports, only the first connectable implicit should connect + for tag in implicit.tags: + for port in block._get_ports_by_tag({tag}): + if port not in already_connected_ports: + self.parent.connect(implicit.port, port) + already_connected_ports.add(port) - return block + return block - def __enter__(self) -> ImplicitScope: - self.open = True - return self + def __enter__(self) -> ImplicitScope: + self.open = True + return self - def __exit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]) -> None: - self.open = False + def __exit__( + self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType] + ) -> None: + self.open = False class ChainConnect: - """Return type of chain connect operation, that can't be used in EDG operations except assignment to instance - variable for naming. - """ - def __init__(self, blocks: List[Block], links: List[Connection]): - self.blocks = blocks - self.links = links + """Return type of chain connect operation, that can't be used in EDG operations except assignment to instance + variable for naming. + """ - def __iter__(self) -> Iterable[Union[Tuple[Block, ...], 'ChainConnect']]: - return iter((tuple(self.blocks), self)) + def __init__(self, blocks: List[Block], links: List[Connection]): + self.blocks = blocks + self.links = links + + def __iter__(self) -> Iterable[Union[Tuple[Block, ...], "ChainConnect"]]: + return iter((tuple(self.blocks), self)) + + +BlockPrototypeType = TypeVar("BlockPrototypeType", bound="Block") -BlockPrototypeType = TypeVar('BlockPrototypeType', bound='Block') class BlockPrototype(Generic[BlockPrototypeType]): - """A block prototype, that contains a type and arguments, but without constructing the entire block - and running its (potentially quite expensive) __init__. - - This class is automatically created on Block instantiations by the BlockMeta metaclass __init__ hook.""" - def __init__(self, tpe: Type[BlockPrototypeType], args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> None: - self._tpe = tpe - self._args = args - self._kwargs = kwargs - - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self._tpe}, args={self._args}, kwargs={self._kwargs})" - - def _bind(self, parent: Union[BaseBlock, Port]) -> BlockPrototypeType: - """Binds the prototype into an actual Block instance.""" - Block._next_bind = self._tpe - block = self._tpe(*self._args, **self._kwargs) - block._bind_in_place(parent) - return block - - @override - def __getattribute__(self, item: str) -> Any: - if item.startswith("_"): - return super().__getattribute__(item) - else: - raise AttributeError(f"{self.__class__.__name__} has no attributes, must bind to get a concrete instance, tried to get {item}") - - @override - def __setattr__(self, key: str, value: Any) -> None: - if key.startswith("_"): - super().__setattr__(key, value) - else: - raise AttributeError(f"{self.__class__.__name__} has no attributes, must bind to get a concrete instance, tried to set {key}") + """A block prototype, that contains a type and arguments, but without constructing the entire block + and running its (potentially quite expensive) __init__. + + This class is automatically created on Block instantiations by the BlockMeta metaclass __init__ hook.""" + + def __init__(self, tpe: Type[BlockPrototypeType], args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> None: + self._tpe = tpe + self._args = args + self._kwargs = kwargs + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self._tpe}, args={self._args}, kwargs={self._kwargs})" + + def _bind(self, parent: Union[BaseBlock, Port]) -> BlockPrototypeType: + """Binds the prototype into an actual Block instance.""" + Block._next_bind = self._tpe + block = self._tpe(*self._args, **self._kwargs) + block._bind_in_place(parent) + return block + + @override + def __getattribute__(self, item: str) -> Any: + if item.startswith("_"): + return super().__getattribute__(item) + else: + raise AttributeError( + f"{self.__class__.__name__} has no attributes, must bind to get a concrete instance, tried to get {item}" + ) + + @override + def __setattr__(self, key: str, value: Any) -> None: + if key.startswith("_"): + super().__setattr__(key, value) + else: + raise AttributeError( + f"{self.__class__.__name__} has no attributes, must bind to get a concrete instance, tried to set {key}" + ) class BlockMeta(BaseBlockMeta): - """This provides a hook on __init__ that replaces argument values with empty ConstraintExpr - based on the type annotation and stores the supplied argument to the __init__ (if any) in the binding. - - The supplied argument is cast to its target type and stored in the binding, in its parent context. - The binding itself is in the new object's context. - - This performs two functions: - - Allows blocks to compile at the top-level where required parameters have no values and there is no - context that provides those values - - Standardize the type of objects passed to self.ArgParameter, so the result is properly typed. - - This is applied to every class that inherits this metaclass, and hooks every super().__init__ call.""" - - _ANNOTATION_EXPR_MAP: Dict[Any, Type[ConstraintExpr]] = { - BoolLike: BoolExpr, - "BoolLike": BoolExpr, - IntLike: IntExpr, - "IntLike": IntExpr, - FloatLike: FloatExpr, - "FloatLike": FloatExpr, - RangeLike: RangeExpr, - "RangeLike": RangeExpr, - StringLike: StringExpr, - "StringLike": StringExpr, - ArrayBoolLike: ArrayBoolExpr, - "ArrayBoolLike": ArrayBoolExpr, - ArrayIntLike: ArrayIntExpr, - "ArrayIntLike": ArrayIntExpr, - ArrayFloatLike: ArrayFloatExpr, - "ArrayFloatLike": ArrayFloatExpr, - ArrayRangeLike: ArrayRangeExpr, - "ArrayRangeLike": ArrayRangeExpr, - ArrayStringLike: ArrayStringExpr, - "ArrayStringLike": ArrayStringExpr, - } - - def __new__(cls, *args: Any, **kwargs: Any) -> Any: - new_cls = super().__new__(cls, *args, **kwargs) - - if '__init__' in new_cls.__dict__: - orig_init = new_cls.__dict__['__init__'] - - # collect and pre-process argument data - arg_data: List[Tuple[str, inspect.Parameter, Type[ConstraintExpr]]] = [] - # discard param 0 (self) - for arg_name, arg_param in list(inspect.signature(orig_init).parameters.items())[1:]: - if arg_param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): - continue # pass through of *args, **kwargs handled later - param_expr_type = BlockMeta._ANNOTATION_EXPR_MAP.get(arg_param.annotation, None) - if param_expr_type is None: - raise BlockDefinitionError(new_cls, f"in {new_cls}.__init__, unknown annotation type for {arg_name}: {arg_param.annotation}") - - arg_data.append((arg_name, arg_param, param_expr_type)) - - def wrapped_init(self: Any, *args: Any, **kwargs: Any) -> None: - if not hasattr(self, '_init_params'): # used to communicate to the block the added init params - self._init_params = {} - - def remap_arg(arg_name: str, arg_type: Type[ConstraintExpr], arg_value: Any) -> ConstraintExpr: - if isinstance(arg_value, ConstraintExpr): - if isinstance(arg_value.binding, InitParamBinding) and arg_value.binding.parent is self: - return arg_value # pass through arg that has been previously transformed - - if isinstance(arg_value, ConstraintExpr): # otherwise, create a new arg - if arg_value._is_bound(): - typed_arg_value: Optional[ConstraintExpr] = arg_value - elif arg_value.initializer is None: - typed_arg_value = None - else: - raise BlockDefinitionError(self, - f"in constructor arguments got non-bound value {arg_name}={arg_value}: " + \ - "either leave default empty or pass in a value or uninitialized type " + \ - "(eg, 2.0, FloatExpr(), but NOT FloatExpr(2.0))") - elif arg_value is not None: - typed_arg_value = arg_value - else: - typed_arg_value = None - - return arg_type()._bind(InitParamBinding(self, typed_arg_value)) - - builder_prev = builder.push_element(self) - try: - # rebuild args and kwargs by traversing the args list - new_args: List[Any] = [] - new_kwargs: Dict[str, Any] = {} - for arg_pos, (arg_name, arg_param, param_expr_type) in enumerate(arg_data): - if arg_pos < len(args) and arg_param.kind in (inspect.Parameter.POSITIONAL_ONLY, - inspect.Parameter.POSITIONAL_OR_KEYWORD): # present positional arg - new_arg = remap_arg(arg_name, param_expr_type, args[arg_pos]) - new_args.append(new_arg) - self._init_params[arg_name] = new_arg - elif arg_pos >= len(args) and arg_param.kind in (inspect.Parameter.POSITIONAL_ONLY, ): # non-present positional arg - if len(builder.stack) == 1: # at top-level, fill in all args - new_arg = remap_arg(arg_name, param_expr_type, None) - new_args.append(new_arg) - self._init_params[arg_name] = new_arg - elif arg_name in kwargs and arg_param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, - inspect.Parameter.KEYWORD_ONLY): # present kwarg - new_arg = remap_arg(arg_name, param_expr_type, kwargs[arg_name]) - new_kwargs[arg_name] = new_arg - self._init_params[arg_name] = new_arg - elif arg_name not in kwargs and arg_param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, - inspect.Parameter.KEYWORD_ONLY): # non-present kwarg - if arg_param.default is not inspect._empty: # default values do show up in kwargs, add them to transform them - new_arg = remap_arg(arg_name, param_expr_type, arg_param.default) - new_kwargs[arg_name] = new_arg - self._init_params[arg_name] = new_arg - elif len(builder.stack) == 1: # at top-level, fill in all args - new_arg = remap_arg(arg_name, param_expr_type, None) - new_kwargs[arg_name] = new_arg - self._init_params[arg_name] = new_arg - - # unconditionally pass through all args and kwargs - new_args.extend(args[len(new_args):]) - for arg_name in kwargs: - if arg_name not in new_kwargs: - new_kwargs[arg_name] = kwargs[arg_name] - - orig_init(self, *new_args, **new_kwargs) - finally: - builder.pop_to(builder_prev) - - new_cls.__init__ = functools.update_wrapper(wrapped_init, orig_init) - - return new_cls + """This provides a hook on __init__ that replaces argument values with empty ConstraintExpr + based on the type annotation and stores the supplied argument to the __init__ (if any) in the binding. + + The supplied argument is cast to its target type and stored in the binding, in its parent context. + The binding itself is in the new object's context. + + This performs two functions: + - Allows blocks to compile at the top-level where required parameters have no values and there is no + context that provides those values + - Standardize the type of objects passed to self.ArgParameter, so the result is properly typed. + + This is applied to every class that inherits this metaclass, and hooks every super().__init__ call.""" + + _ANNOTATION_EXPR_MAP: Dict[Any, Type[ConstraintExpr]] = { + BoolLike: BoolExpr, + "BoolLike": BoolExpr, + IntLike: IntExpr, + "IntLike": IntExpr, + FloatLike: FloatExpr, + "FloatLike": FloatExpr, + RangeLike: RangeExpr, + "RangeLike": RangeExpr, + StringLike: StringExpr, + "StringLike": StringExpr, + ArrayBoolLike: ArrayBoolExpr, + "ArrayBoolLike": ArrayBoolExpr, + ArrayIntLike: ArrayIntExpr, + "ArrayIntLike": ArrayIntExpr, + ArrayFloatLike: ArrayFloatExpr, + "ArrayFloatLike": ArrayFloatExpr, + ArrayRangeLike: ArrayRangeExpr, + "ArrayRangeLike": ArrayRangeExpr, + ArrayStringLike: ArrayStringExpr, + "ArrayStringLike": ArrayStringExpr, + } + + def __new__(cls, *args: Any, **kwargs: Any) -> Any: + new_cls = super().__new__(cls, *args, **kwargs) + + if "__init__" in new_cls.__dict__: + orig_init = new_cls.__dict__["__init__"] + + # collect and pre-process argument data + arg_data: List[Tuple[str, inspect.Parameter, Type[ConstraintExpr]]] = [] + # discard param 0 (self) + for arg_name, arg_param in list(inspect.signature(orig_init).parameters.items())[1:]: + if arg_param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): + continue # pass through of *args, **kwargs handled later + param_expr_type = BlockMeta._ANNOTATION_EXPR_MAP.get(arg_param.annotation, None) + if param_expr_type is None: + raise BlockDefinitionError( + new_cls, + f"in {new_cls}.__init__, unknown annotation type for {arg_name}: {arg_param.annotation}", + ) + + arg_data.append((arg_name, arg_param, param_expr_type)) + + def wrapped_init(self: Any, *args: Any, **kwargs: Any) -> None: + if not hasattr(self, "_init_params"): # used to communicate to the block the added init params + self._init_params = {} + + def remap_arg(arg_name: str, arg_type: Type[ConstraintExpr], arg_value: Any) -> ConstraintExpr: + if isinstance(arg_value, ConstraintExpr): + if isinstance(arg_value.binding, InitParamBinding) and arg_value.binding.parent is self: + return arg_value # pass through arg that has been previously transformed + + if isinstance(arg_value, ConstraintExpr): # otherwise, create a new arg + if arg_value._is_bound(): + typed_arg_value: Optional[ConstraintExpr] = arg_value + elif arg_value.initializer is None: + typed_arg_value = None + else: + raise BlockDefinitionError( + self, + f"in constructor arguments got non-bound value {arg_name}={arg_value}: " + + "either leave default empty or pass in a value or uninitialized type " + + "(eg, 2.0, FloatExpr(), but NOT FloatExpr(2.0))", + ) + elif arg_value is not None: + typed_arg_value = arg_value + else: + typed_arg_value = None + + return arg_type()._bind(InitParamBinding(self, typed_arg_value)) + + builder_prev = builder.push_element(self) + try: + # rebuild args and kwargs by traversing the args list + new_args: List[Any] = [] + new_kwargs: Dict[str, Any] = {} + for arg_pos, (arg_name, arg_param, param_expr_type) in enumerate(arg_data): + if arg_pos < len(args) and arg_param.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ): # present positional arg + new_arg = remap_arg(arg_name, param_expr_type, args[arg_pos]) + new_args.append(new_arg) + self._init_params[arg_name] = new_arg + elif arg_pos >= len(args) and arg_param.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + ): # non-present positional arg + if len(builder.stack) == 1: # at top-level, fill in all args + new_arg = remap_arg(arg_name, param_expr_type, None) + new_args.append(new_arg) + self._init_params[arg_name] = new_arg + elif arg_name in kwargs and arg_param.kind in ( + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ): # present kwarg + new_arg = remap_arg(arg_name, param_expr_type, kwargs[arg_name]) + new_kwargs[arg_name] = new_arg + self._init_params[arg_name] = new_arg + elif arg_name not in kwargs and arg_param.kind in ( + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ): # non-present kwarg + if ( + arg_param.default is not inspect._empty + ): # default values do show up in kwargs, add them to transform them + new_arg = remap_arg(arg_name, param_expr_type, arg_param.default) + new_kwargs[arg_name] = new_arg + self._init_params[arg_name] = new_arg + elif len(builder.stack) == 1: # at top-level, fill in all args + new_arg = remap_arg(arg_name, param_expr_type, None) + new_kwargs[arg_name] = new_arg + self._init_params[arg_name] = new_arg + + # unconditionally pass through all args and kwargs + new_args.extend(args[len(new_args) :]) + for arg_name in kwargs: + if arg_name not in new_kwargs: + new_kwargs[arg_name] = kwargs[arg_name] + + orig_init(self, *new_args, **new_kwargs) + finally: + builder.pop_to(builder_prev) + + new_cls.__init__ = functools.update_wrapper(wrapped_init, orig_init) + + return new_cls @non_library class Block(BaseBlock, metaclass=BlockMeta): - """Part with a statically-defined subcircuit. - Relations between contained parameters may only be expressed in the given constraint language. - """ - _next_bind: Optional[Type[Block]] = None # set when binding, to avoid creating a prototype - - def __new__(cls, *args: Any, **kwargs: Any) -> Block: - if Block._next_bind is not None: - assert Block._next_bind is cls - Block._next_bind = None - return super().__new__(cls) - elif builder.get_enclosing_block() is None: # always construct if top-level - return super().__new__(cls) - else: - return BlockPrototype(cls, args, kwargs) # type: ignore - - SelfType = TypeVar('SelfType', bound='BaseBlock') - def _bind(self: SelfType, parent: Union[BaseBlock, Port]) -> SelfType: - # for type checking only - raise TypeError("_bind should be called from BlockPrototype") - - def __init__(self) -> None: - super().__init__() - - if hasattr(self, '_init_params'): # used to propagate params generated in the metaclass __init__ hook - self._init_params: Dict[str, ConstraintExpr] - for param_name, param in self._init_params.items(): - self._parameters.register(param) - self.manager.add_element(param_name, param) - delattr(self, '_init_params') - - self._mixins: List['BlockInterfaceMixin'] = [] - - self._blocks = self.manager.new_dict(Block) - self._chains = self.manager.new_dict(ChainConnect, anon_prefix='anon_chain') - self._port_tags = IdentityDict[BasePort, Set[PortTag]]() - - def _get_ports_by_tag(self, tags: Set[PortTag]) -> List[BasePort]: - out = [] - for block_port_name, block_port in self._ports.items(): - assert isinstance(block_port, BasePort) - port_tags: Set[PortTag] = self._port_tags.get(block_port, set()) - if port_tags.issuperset(tags): - out.append(block_port) - return out - - @override - def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath, *, - interface_only: bool = False) -> None: - super()._build_ref_map(ref_map, prefix) - for mixin in self._mixins: - mixin._build_ref_map(ref_map, prefix) - if not interface_only: - for name, block in self._blocks.items(): - assert isinstance(block, Block) - block._build_ref_map(ref_map, edgir.localpath_concat(prefix, name), interface_only=True) - - @override - def _populate_def_proto_block_base(self, pb: edgir.BlockLikeTypes) -> None: - super()._populate_def_proto_block_base(pb) - assert isinstance(pb, edgir.HierarchyBlock) - - # generate param defaults - for param_name, param in self._parameters.items(): - if isinstance(param.binding, InitParamBinding) and param.binding.value is not None: - # default values can't depend on anything so the ref_map is empty - param_typed_value = param._to_expr_type(param.binding.value) - param_typed_value._populate_expr_proto(pb.param_defaults[param_name], IdentityDict()) - - def _populate_def_proto_hierarchy(self, pb: edgir.HierarchyBlock, ref_map: Refable.RefMapType) -> None: - self._blocks.finalize() - self._connects.finalize() - self._chains.finalize() - - for name, block in self._blocks.items(): - new_block_lib = edgir.add_pair(pb.blocks, name).lib_elem - new_block_lib.base.target.name = block._get_def_name() - for mixin in block._mixins: - new_block_lib.mixins.add().target.name = mixin._get_def_name() - - # actually generate the links and connects - link_chain_names = IdentityDict[Connection, List[str]]() # prefer chain name where applicable - # TODO generate into primary data structures - for name, chain in self._chains.items_ordered(): # TODO work with net join - for i, connect in enumerate(chain.links): - link_chain_names.setdefault(connect, []).append(f"{name}_{i}") - - delegated_connects = self._all_delegated_connects() - for name, connect in self._connects.items_ordered(): - if connect in delegated_connects: - continue - connect_names_opt = [self._connects.name_of(c) for c in self._all_connects_of(connect)] - connect_names = [c for c in connect_names_opt if c is not None and not c.startswith('anon_')] - if len(connect_names) > 1: - raise UnconnectableError(f"Multiple names {connect_names} for connect") - elif len(connect_names) == 1: - name = connect_names[0] - - connect_elts = connect.make_connection() - - if name.startswith('anon_'): # infer a non-anon name if possible - if connect in link_chain_names and not link_chain_names[connect][0].startswith('anon_'): - name = link_chain_names[connect][0] # arbitrarily pick the first chain name - elif isinstance(connect_elts, Connection.Export): - port_pathname = connect_elts.external_port._name_from(self).replace('.', '_') - name = f'_{port_pathname}_link' - elif isinstance(connect_elts, Connection.ConnectedLink) and connect_elts.bridged_connects: - port_pathname = connect_elts.bridged_connects[0][0]._name_from(self).replace('.', '_') - name = f'_{port_pathname}_link' - elif isinstance(connect_elts, Connection.ConnectedLink) and connect_elts.link_connects: - port_pathname = connect_elts.link_connects[0][0]._name_from(self).replace('.', '_') - name = f'_{port_pathname}_link' - elif connect in link_chain_names: # if there's really nothing better, anon_chain is better than nothing - name = link_chain_names[connect][0] - - if connect_elts is None: # single port net - effectively discard - pass - elif isinstance(connect_elts, Connection.Export): # generate direct export - constraint_pb = edgir.add_pair(pb.constraints, f"(conn){name}") - if connect_elts.is_array: - constraint_pb.exportedArray.exterior_port.ref.CopyFrom(ref_map[connect_elts.external_port]) - constraint_pb.exportedArray.internal_block_port.ref.CopyFrom(ref_map[connect_elts.internal_port]) + """Part with a statically-defined subcircuit. + Relations between contained parameters may only be expressed in the given constraint language. + """ + + _next_bind: Optional[Type[Block]] = None # set when binding, to avoid creating a prototype + + def __new__(cls, *args: Any, **kwargs: Any) -> Block: + if Block._next_bind is not None: + assert Block._next_bind is cls + Block._next_bind = None + return super().__new__(cls) + elif builder.get_enclosing_block() is None: # always construct if top-level + return super().__new__(cls) else: - constraint_pb.exported.exterior_port.ref.CopyFrom(ref_map[connect_elts.external_port]) - constraint_pb.exported.internal_block_port.ref.CopyFrom(ref_map[connect_elts.internal_port]) - - elif isinstance(connect_elts, Connection.ConnectedLink): # generate link - link_path = edgir.localpath_concat(edgir.LocalPath(), name) - link_pb = edgir.add_pair(pb.links, name) - if connect_elts.is_link_array: - link_pb.array.self_class.target.name = connect_elts.link_type._static_def_name() + return BlockPrototype(cls, args, kwargs) # type: ignore + + SelfType = TypeVar("SelfType", bound="BaseBlock") + + def _bind(self: SelfType, parent: Union[BaseBlock, Port]) -> SelfType: + # for type checking only + raise TypeError("_bind should be called from BlockPrototype") + + def __init__(self) -> None: + super().__init__() + + if hasattr(self, "_init_params"): # used to propagate params generated in the metaclass __init__ hook + self._init_params: Dict[str, ConstraintExpr] + for param_name, param in self._init_params.items(): + self._parameters.register(param) + self.manager.add_element(param_name, param) + delattr(self, "_init_params") + + self._mixins: List["BlockInterfaceMixin"] = [] + + self._blocks = self.manager.new_dict(Block) + self._chains = self.manager.new_dict(ChainConnect, anon_prefix="anon_chain") + self._port_tags = IdentityDict[BasePort, Set[PortTag]]() + + def _get_ports_by_tag(self, tags: Set[PortTag]) -> List[BasePort]: + out = [] + for block_port_name, block_port in self._ports.items(): + assert isinstance(block_port, BasePort) + port_tags: Set[PortTag] = self._port_tags.get(block_port, set()) + if port_tags.issuperset(tags): + out.append(block_port) + return out + + @override + def _build_ref_map( + self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath, *, interface_only: bool = False + ) -> None: + super()._build_ref_map(ref_map, prefix) + for mixin in self._mixins: + mixin._build_ref_map(ref_map, prefix) + if not interface_only: + for name, block in self._blocks.items(): + assert isinstance(block, Block) + block._build_ref_map(ref_map, edgir.localpath_concat(prefix, name), interface_only=True) + + @override + def _populate_def_proto_block_base(self, pb: edgir.BlockLikeTypes) -> None: + super()._populate_def_proto_block_base(pb) + assert isinstance(pb, edgir.HierarchyBlock) + + # generate param defaults + for param_name, param in self._parameters.items(): + if isinstance(param.binding, InitParamBinding) and param.binding.value is not None: + # default values can't depend on anything so the ref_map is empty + param_typed_value = param._to_expr_type(param.binding.value) + param_typed_value._populate_expr_proto(pb.param_defaults[param_name], IdentityDict()) + + def _populate_def_proto_hierarchy(self, pb: edgir.HierarchyBlock, ref_map: Refable.RefMapType) -> None: + self._blocks.finalize() + self._connects.finalize() + self._chains.finalize() + + for name, block in self._blocks.items(): + new_block_lib = edgir.add_pair(pb.blocks, name).lib_elem + new_block_lib.base.target.name = block._get_def_name() + for mixin in block._mixins: + new_block_lib.mixins.add().target.name = mixin._get_def_name() + + # actually generate the links and connects + link_chain_names = IdentityDict[Connection, List[str]]() # prefer chain name where applicable + # TODO generate into primary data structures + for name, chain in self._chains.items_ordered(): # TODO work with net join + for i, connect in enumerate(chain.links): + link_chain_names.setdefault(connect, []).append(f"{name}_{i}") + + delegated_connects = self._all_delegated_connects() + for name, connect in self._connects.items_ordered(): + if connect in delegated_connects: + continue + connect_names_opt = [self._connects.name_of(c) for c in self._all_connects_of(connect)] + connect_names = [c for c in connect_names_opt if c is not None and not c.startswith("anon_")] + if len(connect_names) > 1: + raise UnconnectableError(f"Multiple names {connect_names} for connect") + elif len(connect_names) == 1: + name = connect_names[0] + + connect_elts = connect.make_connection() + + if name.startswith("anon_"): # infer a non-anon name if possible + if connect in link_chain_names and not link_chain_names[connect][0].startswith("anon_"): + name = link_chain_names[connect][0] # arbitrarily pick the first chain name + elif isinstance(connect_elts, Connection.Export): + port_pathname = connect_elts.external_port._name_from(self).replace(".", "_") + name = f"_{port_pathname}_link" + elif isinstance(connect_elts, Connection.ConnectedLink) and connect_elts.bridged_connects: + port_pathname = connect_elts.bridged_connects[0][0]._name_from(self).replace(".", "_") + name = f"_{port_pathname}_link" + elif isinstance(connect_elts, Connection.ConnectedLink) and connect_elts.link_connects: + port_pathname = connect_elts.link_connects[0][0]._name_from(self).replace(".", "_") + name = f"_{port_pathname}_link" + elif connect in link_chain_names: # if there's really nothing better, anon_chain is better than nothing + name = link_chain_names[connect][0] + + if connect_elts is None: # single port net - effectively discard + pass + elif isinstance(connect_elts, Connection.Export): # generate direct export + constraint_pb = edgir.add_pair(pb.constraints, f"(conn){name}") + if connect_elts.is_array: + constraint_pb.exportedArray.exterior_port.ref.CopyFrom(ref_map[connect_elts.external_port]) + constraint_pb.exportedArray.internal_block_port.ref.CopyFrom(ref_map[connect_elts.internal_port]) + else: + constraint_pb.exported.exterior_port.ref.CopyFrom(ref_map[connect_elts.external_port]) + constraint_pb.exported.internal_block_port.ref.CopyFrom(ref_map[connect_elts.internal_port]) + + elif isinstance(connect_elts, Connection.ConnectedLink): # generate link + link_path = edgir.localpath_concat(edgir.LocalPath(), name) + link_pb = edgir.add_pair(pb.links, name) + if connect_elts.is_link_array: + link_pb.array.self_class.target.name = connect_elts.link_type._static_def_name() + else: + link_pb.lib_elem.target.name = connect_elts.link_type._static_def_name() + + for idx, (self_port, link_port_path) in enumerate(connect_elts.bridged_connects): + assert isinstance(self_port, Port) + assert not connect_elts.is_link_array, "bridged arrays not supported" + assert self_port.bridge_type is not None + + port_name = self_port._name_from(self) + edgir.add_pair(pb.blocks, f"(bridge){port_name}").lib_elem.base.target.name = ( + self_port.bridge_type._static_def_name() + ) + bridge_path = edgir.localpath_concat(edgir.LocalPath(), f"(bridge){port_name}") + + bridge_constraint_pb = edgir.add_pair(pb.constraints, f"(bridge){name}_b{idx}") + bridge_constraint_pb.exported.exterior_port.ref.CopyFrom(ref_map[self_port]) + bridge_constraint_pb.exported.internal_block_port.ref.CopyFrom( + edgir.localpath_concat(bridge_path, "outer_port") + ) + + connect_constraint_pb = edgir.add_pair(pb.constraints, f"(conn){name}_b{idx}") + connect_constraint_pb.connected.block_port.ref.CopyFrom( + edgir.localpath_concat(bridge_path, "inner_link") + ) + connect_constraint_pb.connected.link_port.ref.CopyFrom( + edgir.localpath_concat(link_path, link_port_path) + ) + + for idx, (subelt_port, link_port_path) in enumerate(connect_elts.link_connects): + connect_constraint_pb = edgir.add_pair(pb.constraints, f"(conn){name}_d{idx}") + if connect_elts.is_link_array: + connect_constraint_pb.connectedArray.block_port.ref.CopyFrom(ref_map[subelt_port]) + connect_constraint_pb.connectedArray.link_port.ref.CopyFrom( + edgir.localpath_concat(link_path, link_port_path) + ) + else: + connect_constraint_pb.connected.block_port.ref.CopyFrom(ref_map[subelt_port]) + connect_constraint_pb.connected.link_port.ref.CopyFrom( + edgir.localpath_concat(link_path, link_port_path) + ) + else: + raise ValueError("unknown connect type") + + # generate block initializers + for block_name, block in self._blocks.items(): + all_block_params = dict(block._parameters.items()) + for mixin in block._mixins: + all_block_params.update(mixin._parameters.items()) + for block_param_name, block_param in all_block_params.items(): + if isinstance(block_param.binding, InitParamBinding) and block_param.binding.value is not None: + param_typed_value = block_param._to_expr_type(block_param.binding.value) + AssignBinding.populate_assign_proto( + edgir.add_pair(pb.constraints, f"(init){block_name}.{block_param_name}"), + block_param, + param_typed_value, + ref_map, + ) + + # TODO make this non-overriding? + @override + def _def_to_proto(self) -> edgir.HierarchyBlock: + assert not self._mixins # blocks with mixins can only be instantiated anonymously + ref_map = self._create_ref_map() + + pb = edgir.HierarchyBlock() + pb.prerefine_class.target.name = ( + self._get_def_name() + ) # TODO integrate with a non-link populate_def_proto_block... + self._populate_def_proto_block_base(pb) + self._populate_def_proto_port_init(pb, ref_map) + + self._populate_def_proto_param_init(pb, ref_map) + + self._populate_def_proto_hierarchy(pb, ref_map) + self._populate_def_proto_block_contents(pb, ref_map) + self._populate_def_proto_description(pb, ref_map) + + return pb + + @override + def _elaborated_def_to_proto(self) -> edgir.HierarchyBlock: + return cast(edgir.HierarchyBlock, super()._elaborated_def_to_proto()) + + MixinType = TypeVar("MixinType", bound="BlockInterfaceMixin") + + def with_mixin(self, tpe: MixinType) -> MixinType: + """Adds an interface mixin for this Block. Mainly useful for abstract blocks, e.g. IoController with HasI2s.""" + from .BlockInterfaceMixin import BlockInterfaceMixin + + if isinstance(tpe, BlockPrototype): + tpe_cls = tpe._tpe else: - link_pb.lib_elem.target.name = connect_elts.link_type._static_def_name() - - for idx, (self_port, link_port_path) in enumerate(connect_elts.bridged_connects): - assert isinstance(self_port, Port) - assert not connect_elts.is_link_array, "bridged arrays not supported" - assert self_port.bridge_type is not None - - port_name = self_port._name_from(self) - edgir.add_pair(pb.blocks, f"(bridge){port_name}").lib_elem.base.target.name = self_port.bridge_type._static_def_name() - bridge_path = edgir.localpath_concat(edgir.LocalPath(), f"(bridge){port_name}") - - bridge_constraint_pb = edgir.add_pair(pb.constraints, f"(bridge){name}_b{idx}") - bridge_constraint_pb.exported.exterior_port.ref.CopyFrom(ref_map[self_port]) - bridge_constraint_pb.exported.internal_block_port.ref.CopyFrom(edgir.localpath_concat(bridge_path, 'outer_port')) - - connect_constraint_pb = edgir.add_pair(pb.constraints, f"(conn){name}_b{idx}") - connect_constraint_pb.connected.block_port.ref.CopyFrom(edgir.localpath_concat(bridge_path, 'inner_link')) - connect_constraint_pb.connected.link_port.ref.CopyFrom(edgir.localpath_concat(link_path, link_port_path)) - - for idx, (subelt_port, link_port_path) in enumerate(connect_elts.link_connects): - connect_constraint_pb = edgir.add_pair(pb.constraints, f"(conn){name}_d{idx}") - if connect_elts.is_link_array: - connect_constraint_pb.connectedArray.block_port.ref.CopyFrom(ref_map[subelt_port]) - connect_constraint_pb.connectedArray.link_port.ref.CopyFrom(edgir.localpath_concat(link_path, link_port_path)) - else: - connect_constraint_pb.connected.block_port.ref.CopyFrom(ref_map[subelt_port]) - connect_constraint_pb.connected.link_port.ref.CopyFrom(edgir.localpath_concat(link_path, link_port_path)) - else: - raise ValueError("unknown connect type") - - # generate block initializers - for (block_name, block) in self._blocks.items(): - all_block_params = dict(block._parameters.items()) - for mixin in block._mixins: - all_block_params.update(mixin._parameters.items()) - for (block_param_name, block_param) in all_block_params.items(): - if isinstance(block_param.binding, InitParamBinding) and block_param.binding.value is not None: - param_typed_value = block_param._to_expr_type(block_param.binding.value) - AssignBinding.populate_assign_proto(edgir.add_pair(pb.constraints, f'(init){block_name}.{block_param_name}'), - block_param, param_typed_value, ref_map) - - - # TODO make this non-overriding? - @override - def _def_to_proto(self) -> edgir.HierarchyBlock: - assert not self._mixins # blocks with mixins can only be instantiated anonymously - ref_map = self._create_ref_map() - - pb = edgir.HierarchyBlock() - pb.prerefine_class.target.name = self._get_def_name() # TODO integrate with a non-link populate_def_proto_block... - self._populate_def_proto_block_base(pb) - self._populate_def_proto_port_init(pb, ref_map) - - self._populate_def_proto_param_init(pb, ref_map) - - self._populate_def_proto_hierarchy(pb, ref_map) - self._populate_def_proto_block_contents(pb, ref_map) - self._populate_def_proto_description(pb, ref_map) - - return pb - - @override - def _elaborated_def_to_proto(self) -> edgir.HierarchyBlock: - return cast(edgir.HierarchyBlock, super()._elaborated_def_to_proto()) - - MixinType = TypeVar('MixinType', bound='BlockInterfaceMixin') - def with_mixin(self, tpe: MixinType) -> MixinType: - """Adds an interface mixin for this Block. Mainly useful for abstract blocks, e.g. IoController with HasI2s.""" - from .BlockInterfaceMixin import BlockInterfaceMixin - if isinstance(tpe, BlockPrototype): - tpe_cls = tpe._tpe - else: - tpe_cls = tpe.__class__ - - if not (issubclass(tpe_cls, BlockInterfaceMixin) and tpe_cls._is_mixin()): - raise EdgTypeError("with_mixin param", tpe, BlockInterfaceMixin) - if isinstance(self, BlockInterfaceMixin) and self._is_mixin(): - raise BlockDefinitionError(type(self), "mixins can not have with_mixin") - if (self.__class__, AbstractBlockProperty) not in self._elt_properties: - raise BlockDefinitionError(type(self), "mixins can only be added to abstract classes") - if not isinstance(self, tpe_cls._get_mixin_base()): - raise EdgTypeError(f"block not an instance of mixin base", self, tpe_cls._get_mixin_base()) - assert self._parent is not None - - elt = tpe._bind(self._parent) - self._parent.manager.add_alias(elt, self) - self._mixins.append(elt) - - return elt - - def chain(self, *elts: Union[Connection, BasePort, Block]) -> Any: - if not elts: - return self._chains.register(ChainConnect([], [])) - chain_blocks = [] - chain_links = [] - - if isinstance(elts[0], (BasePort, Connection)): - current_port = elts[0] - elif isinstance(elts[0], Block): - outable_ports = elts[0]._get_ports_by_tag({Output}) + elts[0]._get_ports_by_tag({InOut}) - if len(outable_ports) > 1: - raise BlockDefinitionError(type(self), f"first element 0 to chain {type(elts[0])} does not have exactly one InOut or Output port: {outable_ports}") - current_port = outable_ports[0] - chain_blocks.append(elts[0]) - else: - raise EdgTypeError(f"first element 0 to chain", elts[0], (BasePort, Connection, Block)) - - for i, elt in list(enumerate(elts))[1:-1]: - if not isinstance(elt, Block): - raise EdgTypeError(f"middle arguments elts[{i}] in chain", elt, Block) - if elt._get_ports_by_tag({Input}) and elt._get_ports_by_tag({Output}): - in_ports = elt._get_ports_by_tag({Input}) - out_ports = elt._get_ports_by_tag({Output}) - if len(in_ports) != 1: - raise ChainError(type(self), f"element {i} to chain {type(elt)} does not have exactly one Input port: {in_ports}") - elif len(out_ports) != 1: - raise ChainError(type(self), f"element {i} to chain {type(elt)} does not have exactly one Output port: {out_ports}") - chain_links.append(self.connect(current_port, in_ports[0])) - chain_blocks.append(elt) - current_port = out_ports[0] - elif elt._get_ports_by_tag({InOut}): - ports = elt._get_ports_by_tag({InOut}) - if len(ports) != 1: - raise ChainError(type(self), f"element {i} to chain {type(elt)} does not have exactly one InOut port: {ports}") - self.connect(current_port, ports[0]) - chain_blocks.append(elt) - else: - raise ChainError(type(self), f"element {i} to chain {type(elt)} has no Input and Output, or InOut ports") - - if isinstance(elts[-1], (BasePort, Connection)): - chain_links.append(self.connect(current_port, elts[-1])) - elif isinstance(elts[-1], Block): - inable_ports = elts[-1]._get_ports_by_tag({Input}) + elts[-1]._get_ports_by_tag({InOut}) - if len(inable_ports) != 1: - raise BlockDefinitionError(type(self), f"last element {len(elts) - 1} to chain {type(elts[-1])} does not have exactly one InOut or Input port: {inable_ports}") - chain_blocks.append(elts[-1]) - chain_links.append(self.connect(current_port, inable_ports[0])) - else: - raise EdgTypeError(f"last argument {len(elts) - 1} to chain", elts[-1], (BasePort, Connection, Block)) - - return self._chains.register(ChainConnect(chain_blocks, chain_links)) - - def implicit_connect(self, *implicits: ImplicitConnect) -> ImplicitScope: - for implicit in implicits: - if not isinstance(implicit, ImplicitConnect): - raise EdgTypeError(f"implicit_connect(...) param", implicit, ImplicitConnect) - - return ImplicitScope(self, implicits) - - @override - def connect(self, *connects: Union[BasePort, Connection], flatten: bool=False) -> Connection: - assert not flatten, "flatten only allowed in links" - return super().connect(*connects, flatten=flatten) - - CastableType = TypeVar('CastableType') - from .ConstraintExpr import BoolLike, BoolExpr, FloatLike, FloatExpr, RangeLike, RangeExpr - # type ignore is needed because IntLike overlaps BoolLike - @overload - def ArgParameter(self, param: BoolLike, *, doc: Optional[str] = None) -> BoolExpr: ... # type: ignore - @overload - def ArgParameter(self, param: IntLike, *, doc: Optional[str] = None) -> IntExpr: ... # type: ignore - @overload - def ArgParameter(self, param: FloatLike, *, doc: Optional[str] = None) -> FloatExpr: ... - @overload - def ArgParameter(self, param: RangeLike, *, doc: Optional[str] = None) -> RangeExpr: ... # type: ignore - @overload - def ArgParameter(self, param: StringLike, *, doc: Optional[str] = None) -> StringExpr: ... # type: ignore - @overload - def ArgParameter(self, param: ArrayBoolLike, *, doc: Optional[str] = None) -> ArrayBoolExpr: ... # type: ignore - @overload - def ArgParameter(self, param: ArrayIntLike, *, doc: Optional[str] = None) -> ArrayIntExpr: ... # type: ignore - @overload - def ArgParameter(self, param: ArrayFloatLike, *, doc: Optional[str] = None) -> ArrayFloatExpr: ... - @overload - def ArgParameter(self, param: ArrayRangeLike, *, doc: Optional[str] = None) -> ArrayRangeExpr: ... - @overload - def ArgParameter(self, param: ArrayStringLike, *, doc: Optional[str] = None) -> ArrayStringExpr: ... - - def ArgParameter(self, param: CastableType, *, doc: Optional[str] = None) -> ConstraintExpr[Any, CastableType]: - """Registers a constructor argument parameter for this Block. - This doesn't actually do anything, but is needed to help the type system converter the *Like to a *Expr.""" - if not isinstance(param, ConstraintExpr): - raise EdgTypeError(f"ArgParameter(...) param", param, ConstraintExpr) - if param.binding is None: - raise TypeError(f"param to ArgParameter(...) must have binding") - if not isinstance(param.binding, InitParamBinding): - raise TypeError(f"param to ArgParameter(...) must be __init__ argument") - - if doc is not None: - self._param_docs[param] = doc - - return param - - T = TypeVar('T', bound=BasePort) - @override - def Port(self, tpe: T, tags: Iterable[PortTag]=[], *, optional: bool = False, doc: Optional[str] = None) -> T: - """Registers a port for this Block""" - if not isinstance(tpe, (Port, Vector)): - raise NotImplementedError("Non-Port (eg, Vector) ports not (yet?) supported") - for tag in tags: - if not isinstance(tag, PortTag): - raise EdgTypeError(f"Port(...) tag", tag, PortTag) - - port = super().Port(tpe, optional=optional, doc=doc) - - self._port_tags[port] = set(tags) - return port - - ExportType = TypeVar('ExportType', bound=BasePort) - def Export(self, port: ExportType, tags: Iterable[PortTag]=[], *, optional: bool = False, doc: Optional[str] = None, - _connect: bool = True) -> ExportType: - """Exports a port of a child block, but does not propagate tags or optional.""" - assert port._is_bound(), "can only export bound type" - port_parent = port._block_parent() - assert isinstance(port_parent, Block) - assert port_parent._parent is self, "can only export ports of contained block" - - if isinstance(port, BaseVector): # TODO can the vector and non-vector paths be unified? - assert isinstance(port, Vector) - new_port: BasePort = self.Port(Vector(port._tpe.empty()), - tags, optional=optional, doc=doc) - elif isinstance(port, Port): - new_port = self.Port(type(port).empty(), # TODO is dropping args safe in all cases? - tags, optional=optional, doc=doc) - else: - raise NotImplementedError(f"unknown exported port type {port}") - - if _connect: - self.connect(new_port, port) - - return new_port # type: ignore - - BlockType = TypeVar('BlockType', bound='Block') - def Block(self, tpe: BlockType) -> BlockType: - from .BlockInterfaceMixin import BlockInterfaceMixin - from .DesignTop import DesignTop + tpe_cls = tpe.__class__ + + if not (issubclass(tpe_cls, BlockInterfaceMixin) and tpe_cls._is_mixin()): + raise EdgTypeError("with_mixin param", tpe, BlockInterfaceMixin) + if isinstance(self, BlockInterfaceMixin) and self._is_mixin(): + raise BlockDefinitionError(type(self), "mixins can not have with_mixin") + if (self.__class__, AbstractBlockProperty) not in self._elt_properties: + raise BlockDefinitionError(type(self), "mixins can only be added to abstract classes") + if not isinstance(self, tpe_cls._get_mixin_base()): + raise EdgTypeError(f"block not an instance of mixin base", self, tpe_cls._get_mixin_base()) + assert self._parent is not None + + elt = tpe._bind(self._parent) + self._parent.manager.add_alias(elt, self) + self._mixins.append(elt) + + return elt + + def chain(self, *elts: Union[Connection, BasePort, Block]) -> Any: + if not elts: + return self._chains.register(ChainConnect([], [])) + chain_blocks = [] + chain_links = [] + + if isinstance(elts[0], (BasePort, Connection)): + current_port = elts[0] + elif isinstance(elts[0], Block): + outable_ports = elts[0]._get_ports_by_tag({Output}) + elts[0]._get_ports_by_tag({InOut}) + if len(outable_ports) > 1: + raise BlockDefinitionError( + type(self), + f"first element 0 to chain {type(elts[0])} does not have exactly one InOut or Output port: {outable_ports}", + ) + current_port = outable_ports[0] + chain_blocks.append(elts[0]) + else: + raise EdgTypeError(f"first element 0 to chain", elts[0], (BasePort, Connection, Block)) + + for i, elt in list(enumerate(elts))[1:-1]: + if not isinstance(elt, Block): + raise EdgTypeError(f"middle arguments elts[{i}] in chain", elt, Block) + if elt._get_ports_by_tag({Input}) and elt._get_ports_by_tag({Output}): + in_ports = elt._get_ports_by_tag({Input}) + out_ports = elt._get_ports_by_tag({Output}) + if len(in_ports) != 1: + raise ChainError( + type(self), f"element {i} to chain {type(elt)} does not have exactly one Input port: {in_ports}" + ) + elif len(out_ports) != 1: + raise ChainError( + type(self), + f"element {i} to chain {type(elt)} does not have exactly one Output port: {out_ports}", + ) + chain_links.append(self.connect(current_port, in_ports[0])) + chain_blocks.append(elt) + current_port = out_ports[0] + elif elt._get_ports_by_tag({InOut}): + ports = elt._get_ports_by_tag({InOut}) + if len(ports) != 1: + raise ChainError( + type(self), f"element {i} to chain {type(elt)} does not have exactly one InOut port: {ports}" + ) + self.connect(current_port, ports[0]) + chain_blocks.append(elt) + else: + raise ChainError( + type(self), f"element {i} to chain {type(elt)} has no Input and Output, or InOut ports" + ) + + if isinstance(elts[-1], (BasePort, Connection)): + chain_links.append(self.connect(current_port, elts[-1])) + elif isinstance(elts[-1], Block): + inable_ports = elts[-1]._get_ports_by_tag({Input}) + elts[-1]._get_ports_by_tag({InOut}) + if len(inable_ports) != 1: + raise BlockDefinitionError( + type(self), + f"last element {len(elts) - 1} to chain {type(elts[-1])} does not have exactly one InOut or Input port: {inable_ports}", + ) + chain_blocks.append(elts[-1]) + chain_links.append(self.connect(current_port, inable_ports[0])) + else: + raise EdgTypeError(f"last argument {len(elts) - 1} to chain", elts[-1], (BasePort, Connection, Block)) + + return self._chains.register(ChainConnect(chain_blocks, chain_links)) + + def implicit_connect(self, *implicits: ImplicitConnect) -> ImplicitScope: + for implicit in implicits: + if not isinstance(implicit, ImplicitConnect): + raise EdgTypeError(f"implicit_connect(...) param", implicit, ImplicitConnect) + + return ImplicitScope(self, implicits) + + @override + def connect(self, *connects: Union[BasePort, Connection], flatten: bool = False) -> Connection: + assert not flatten, "flatten only allowed in links" + return super().connect(*connects, flatten=flatten) + + CastableType = TypeVar("CastableType") + from .ConstraintExpr import BoolLike, BoolExpr, FloatLike, FloatExpr, RangeLike, RangeExpr + + # type ignore is needed because IntLike overlaps BoolLike + @overload + def ArgParameter(self, param: BoolLike, *, doc: Optional[str] = None) -> BoolExpr: ... # type: ignore + @overload + def ArgParameter(self, param: IntLike, *, doc: Optional[str] = None) -> IntExpr: ... # type: ignore + @overload + def ArgParameter(self, param: FloatLike, *, doc: Optional[str] = None) -> FloatExpr: ... + @overload + def ArgParameter(self, param: RangeLike, *, doc: Optional[str] = None) -> RangeExpr: ... # type: ignore + @overload + def ArgParameter(self, param: StringLike, *, doc: Optional[str] = None) -> StringExpr: ... # type: ignore + @overload + def ArgParameter(self, param: ArrayBoolLike, *, doc: Optional[str] = None) -> ArrayBoolExpr: ... # type: ignore + @overload + def ArgParameter(self, param: ArrayIntLike, *, doc: Optional[str] = None) -> ArrayIntExpr: ... # type: ignore + @overload + def ArgParameter(self, param: ArrayFloatLike, *, doc: Optional[str] = None) -> ArrayFloatExpr: ... + @overload + def ArgParameter(self, param: ArrayRangeLike, *, doc: Optional[str] = None) -> ArrayRangeExpr: ... + @overload + def ArgParameter(self, param: ArrayStringLike, *, doc: Optional[str] = None) -> ArrayStringExpr: ... + + def ArgParameter(self, param: CastableType, *, doc: Optional[str] = None) -> ConstraintExpr[Any, CastableType]: + """Registers a constructor argument parameter for this Block. + This doesn't actually do anything, but is needed to help the type system converter the *Like to a *Expr.""" + if not isinstance(param, ConstraintExpr): + raise EdgTypeError(f"ArgParameter(...) param", param, ConstraintExpr) + if param.binding is None: + raise TypeError(f"param to ArgParameter(...) must have binding") + if not isinstance(param.binding, InitParamBinding): + raise TypeError(f"param to ArgParameter(...) must be __init__ argument") + + if doc is not None: + self._param_docs[param] = doc + + return param + + T = TypeVar("T", bound=BasePort) + + @override + def Port(self, tpe: T, tags: Iterable[PortTag] = [], *, optional: bool = False, doc: Optional[str] = None) -> T: + """Registers a port for this Block""" + if not isinstance(tpe, (Port, Vector)): + raise NotImplementedError("Non-Port (eg, Vector) ports not (yet?) supported") + for tag in tags: + if not isinstance(tag, PortTag): + raise EdgTypeError(f"Port(...) tag", tag, PortTag) + + port = super().Port(tpe, optional=optional, doc=doc) + + self._port_tags[port] = set(tags) + return port + + ExportType = TypeVar("ExportType", bound=BasePort) + + def Export( + self, + port: ExportType, + tags: Iterable[PortTag] = [], + *, + optional: bool = False, + doc: Optional[str] = None, + _connect: bool = True, + ) -> ExportType: + """Exports a port of a child block, but does not propagate tags or optional.""" + assert port._is_bound(), "can only export bound type" + port_parent = port._block_parent() + assert isinstance(port_parent, Block) + assert port_parent._parent is self, "can only export ports of contained block" + + if isinstance(port, BaseVector): # TODO can the vector and non-vector paths be unified? + assert isinstance(port, Vector) + new_port: BasePort = self.Port(Vector(port._tpe.empty()), tags, optional=optional, doc=doc) + elif isinstance(port, Port): + new_port = self.Port( + type(port).empty(), tags, optional=optional, doc=doc # TODO is dropping args safe in all cases? + ) + else: + raise NotImplementedError(f"unknown exported port type {port}") - if self._elaboration_state not in \ - [BlockElaborationState.init, BlockElaborationState.contents, BlockElaborationState.generate]: - raise BlockDefinitionError(type(self), "can only define blocks in init, contents, or generate") + if _connect: + self.connect(new_port, port) - if isinstance(tpe, BlockPrototype): - tpe_cls = tpe._tpe - else: - tpe_cls = tpe.__class__ + return new_port # type: ignore - if not issubclass(tpe_cls, Block): - raise EdgTypeError(f"Block(...) param", tpe_cls, Block) - if issubclass(tpe_cls, BlockInterfaceMixin) and tpe_cls._is_mixin(): - raise TypeError("param to Block(...) must not be BlockInterfaceMixin") - if issubclass(tpe_cls, DesignTop): - raise TypeError(f"param to Block(...) may not be DesignTop") + BlockType = TypeVar("BlockType", bound="Block") - elt = tpe._bind(self) - self._blocks.register(elt) + def Block(self, tpe: BlockType) -> BlockType: + from .BlockInterfaceMixin import BlockInterfaceMixin + from .DesignTop import DesignTop - return elt + if self._elaboration_state not in [ + BlockElaborationState.init, + BlockElaborationState.contents, + BlockElaborationState.generate, + ]: + raise BlockDefinitionError(type(self), "can only define blocks in init, contents, or generate") + if isinstance(tpe, BlockPrototype): + tpe_cls = tpe._tpe + else: + tpe_cls = tpe.__class__ -AbstractBlockType = TypeVar('AbstractBlockType', bound=Type[Block]) -def abstract_block(decorated: AbstractBlockType) -> AbstractBlockType: - """Defines the decorated block as abstract, a supertype block missing an implementation and - should be refined by a subclass in a final design. - If this block is present (unrefined) in a final design, causes an error.""" - from .BlockInterfaceMixin import BlockInterfaceMixin - if isinstance(decorated, BlockInterfaceMixin) and decorated._is_mixin(): - raise BlockDefinitionError(decorated, "BlockInterfaceMixin @abstract_block definition is redundant") - decorated._elt_properties[(decorated, AbstractBlockProperty)] = None - return decorated + if not issubclass(tpe_cls, Block): + raise EdgTypeError(f"Block(...) param", tpe_cls, Block) + if issubclass(tpe_cls, BlockInterfaceMixin) and tpe_cls._is_mixin(): + raise TypeError("param to Block(...) must not be BlockInterfaceMixin") + if issubclass(tpe_cls, DesignTop): + raise TypeError(f"param to Block(...) may not be DesignTop") + elt = tpe._bind(self) + self._blocks.register(elt) -def abstract_block_default(target: Callable[[], Type[Block]]) -> Callable[[AbstractBlockType], AbstractBlockType]: - """Similar to the abstract_block decorator, but specifies a default refinement. - The argument is a lambda since the default refinement is going to be a subclass of the class being defined, - it will not be defined yet when the base class is being evaluated, so evaluation needs to be delayed.""" - def inner(decorated: AbstractBlockType) -> AbstractBlockType: + return elt + + +AbstractBlockType = TypeVar("AbstractBlockType", bound=Type[Block]) + + +def abstract_block(decorated: AbstractBlockType) -> AbstractBlockType: + """Defines the decorated block as abstract, a supertype block missing an implementation and + should be refined by a subclass in a final design. + If this block is present (unrefined) in a final design, causes an error.""" from .BlockInterfaceMixin import BlockInterfaceMixin + if isinstance(decorated, BlockInterfaceMixin) and decorated._is_mixin(): - raise BlockDefinitionError(decorated, "BlockInterfaceMixin @abstract_block definition is redundant") - decorated._elt_properties[(decorated, AbstractBlockProperty)] = target + raise BlockDefinitionError(decorated, "BlockInterfaceMixin @abstract_block definition is redundant") + decorated._elt_properties[(decorated, AbstractBlockProperty)] = None return decorated - return inner + + +def abstract_block_default(target: Callable[[], Type[Block]]) -> Callable[[AbstractBlockType], AbstractBlockType]: + """Similar to the abstract_block decorator, but specifies a default refinement. + The argument is a lambda since the default refinement is going to be a subclass of the class being defined, + it will not be defined yet when the base class is being evaluated, so evaluation needs to be delayed.""" + + def inner(decorated: AbstractBlockType) -> AbstractBlockType: + from .BlockInterfaceMixin import BlockInterfaceMixin + + if isinstance(decorated, BlockInterfaceMixin) and decorated._is_mixin(): + raise BlockDefinitionError(decorated, "BlockInterfaceMixin @abstract_block definition is redundant") + decorated._elt_properties[(decorated, AbstractBlockProperty)] = target + return decorated + + return inner diff --git a/edg/core/IdentityDict.py b/edg/core/IdentityDict.py index 5ca62df7b..0d4bbd11a 100644 --- a/edg/core/IdentityDict.py +++ b/edg/core/IdentityDict.py @@ -4,74 +4,77 @@ from typing_extensions import override -KeyType = TypeVar('KeyType') -ValueType = TypeVar('ValueType') +KeyType = TypeVar("KeyType") +ValueType = TypeVar("ValueType") + + class IdentityDict(Generic[KeyType, ValueType]): # TODO this should implement Mapping[KeyType, ValueType] - def __init__(self, *args: Union[Iterable[Tuple[KeyType, ValueType]], IdentityDict[KeyType, ValueType]]): - self.dict: Dict[int, ValueType] = {} - self.keys_dict: Dict[int, KeyType] = {} # allow iteration over items - - for elt in args: - if isinstance(elt, IdentityDict): - self.extend(elt.items()) - elif isinstance(elt, Iterable): - self.extend(elt) - - def __getitem__(self, key: KeyType) -> ValueType: - key_id = id(key) - assert key_id in self.dict, "key %s (%s) not found" % (key_id, key) - return self.dict[key_id] - - GetDefaultType = TypeVar('GetDefaultType') - def get(self, key: KeyType, default: GetDefaultType) -> Union[ValueType, GetDefaultType]: - key_id = id(key) - if key_id not in self.dict: - return default - else: - return self.dict[key_id] - - def setdefault(self, key: KeyType, default: ValueType) -> ValueType: - key_id = id(key) - if key_id not in self.dict: - self.dict[key_id] = default - self.keys_dict[key_id] = key - return self.dict[key_id] - - @override - def __repr__(self) -> str: - return "IdentityDict" + str(self.items()) - - def __add__(self, other: IdentityDict[KeyType, ValueType]) -> IdentityDict[KeyType, ValueType]: - return self.extend(other.items()) - - def __setitem__(self, key: KeyType, item: ValueType) -> None: - key_id = id(key) - assert key_id not in self.dict, f"attempted to overwrite {key}={self.dict[key_id]} with new {item}" - self.dict[key_id] = item - self.keys_dict[key_id] = key - - def update(self, key: KeyType, item: ValueType) -> None: - key_id = id(key) - assert key_id in self.dict, f"attempted to update {key}={self.dict[key_id]} with no prior" - self.dict[key_id] = item - self.keys_dict[key_id] = key - - def extend(self, items: Iterable[Tuple[KeyType, ValueType]]) -> IdentityDict[KeyType, ValueType]: - for (key, value) in items: - self[key] = value - return self - - def items(self) -> Iterable[Tuple[KeyType, ValueType]]: - return [(self.keys_dict[key_id], value) for (key_id, value) in self.dict.items()] - - def keys(self) -> Iterable[KeyType]: - return self.keys_dict.values() - - def values(self) -> Iterable[ValueType]: - return self.dict.values() - - def __contains__(self, item: KeyType) -> bool: - return id(item) in self.dict - - def __bool__(self) -> bool: - return bool(self.dict) + def __init__(self, *args: Union[Iterable[Tuple[KeyType, ValueType]], IdentityDict[KeyType, ValueType]]): + self.dict: Dict[int, ValueType] = {} + self.keys_dict: Dict[int, KeyType] = {} # allow iteration over items + + for elt in args: + if isinstance(elt, IdentityDict): + self.extend(elt.items()) + elif isinstance(elt, Iterable): + self.extend(elt) + + def __getitem__(self, key: KeyType) -> ValueType: + key_id = id(key) + assert key_id in self.dict, "key %s (%s) not found" % (key_id, key) + return self.dict[key_id] + + GetDefaultType = TypeVar("GetDefaultType") + + def get(self, key: KeyType, default: GetDefaultType) -> Union[ValueType, GetDefaultType]: + key_id = id(key) + if key_id not in self.dict: + return default + else: + return self.dict[key_id] + + def setdefault(self, key: KeyType, default: ValueType) -> ValueType: + key_id = id(key) + if key_id not in self.dict: + self.dict[key_id] = default + self.keys_dict[key_id] = key + return self.dict[key_id] + + @override + def __repr__(self) -> str: + return "IdentityDict" + str(self.items()) + + def __add__(self, other: IdentityDict[KeyType, ValueType]) -> IdentityDict[KeyType, ValueType]: + return self.extend(other.items()) + + def __setitem__(self, key: KeyType, item: ValueType) -> None: + key_id = id(key) + assert key_id not in self.dict, f"attempted to overwrite {key}={self.dict[key_id]} with new {item}" + self.dict[key_id] = item + self.keys_dict[key_id] = key + + def update(self, key: KeyType, item: ValueType) -> None: + key_id = id(key) + assert key_id in self.dict, f"attempted to update {key}={self.dict[key_id]} with no prior" + self.dict[key_id] = item + self.keys_dict[key_id] = key + + def extend(self, items: Iterable[Tuple[KeyType, ValueType]]) -> IdentityDict[KeyType, ValueType]: + for key, value in items: + self[key] = value + return self + + def items(self) -> Iterable[Tuple[KeyType, ValueType]]: + return [(self.keys_dict[key_id], value) for (key_id, value) in self.dict.items()] + + def keys(self) -> Iterable[KeyType]: + return self.keys_dict.values() + + def values(self) -> Iterable[ValueType]: + return self.dict.values() + + def __contains__(self, item: KeyType) -> bool: + return id(item) in self.dict + + def __bool__(self) -> bool: + return bool(self.dict) diff --git a/edg/core/IdentitySet.py b/edg/core/IdentitySet.py index ad83b95cd..8e33dd3dd 100644 --- a/edg/core/IdentitySet.py +++ b/edg/core/IdentitySet.py @@ -4,50 +4,52 @@ from typing_extensions import override -EltType = TypeVar('EltType') # TODO maybe should be covariant? but is mutable so maybe not? +EltType = TypeVar("EltType") # TODO maybe should be covariant? but is mutable so maybe not? + + class IdentitySet(Generic[EltType]): # TODO this should implement some kind of set base class - def __init__(self, *args: EltType) -> None: - self.set: Set[int] = set() # TODO convenience hack, the same data is in self.dict.keys - self.dict: Dict[int, EltType] = {} + def __init__(self, *args: EltType) -> None: + self.set: Set[int] = set() # TODO convenience hack, the same data is in self.dict.keys + self.dict: Dict[int, EltType] = {} - for elt in args: - self.add(elt) + for elt in args: + self.add(elt) - def __iter__(self) -> Iterator[EltType]: - return iter(self.dict.values()) + def __iter__(self) -> Iterator[EltType]: + return iter(self.dict.values()) - def __len__(self) -> int: - return len(self.set) + def __len__(self) -> int: + return len(self.set) - @override - def __repr__(self) -> str: - return "IdentitySet" + str(self.dict.values()) + @override + def __repr__(self) -> str: + return "IdentitySet" + str(self.dict.values()) - def add(self, item: EltType) -> None: - if id(item) in self.dict: - assert self.dict[id(item)] is item - self.set.add(id(item)) - self.dict[id(item)] = item + def add(self, item: EltType) -> None: + if id(item) in self.dict: + assert self.dict[id(item)] is item + self.set.add(id(item)) + self.dict[id(item)] = item - def __contains__(self, item: EltType) -> bool: - return id(item) in self.set and self.dict[id(item)] is item + def __contains__(self, item: EltType) -> bool: + return id(item) in self.set and self.dict[id(item)] is item - def pop(self) -> EltType: - return self.dict.pop(self.set.pop()) + def pop(self) -> EltType: + return self.dict.pop(self.set.pop()) - def difference_update(self, other: IdentitySet[EltType]) -> None: - self.set.difference_update(other.set) - for elt_id in [elt_id for elt_id in self.dict.keys() if elt_id not in self.set]: - self.dict.pop(elt_id) + def difference_update(self, other: IdentitySet[EltType]) -> None: + self.set.difference_update(other.set) + for elt_id in [elt_id for elt_id in self.dict.keys() if elt_id not in self.set]: + self.dict.pop(elt_id) - def update(self, other: Iterable[EltType]) -> None: - for elt in other: - self.add(elt) + def update(self, other: Iterable[EltType]) -> None: + for elt in other: + self.add(elt) - def get_only(self) -> EltType: # sadly there's no equivalent in Python base set - assert len(self.set) == 1 - return self.dict[next(iter(self.set))] + def get_only(self) -> EltType: # sadly there's no equivalent in Python base set + assert len(self.set) == 1 + return self.dict[next(iter(self.set))] - def remove(self, element: EltType) -> None: - self.set.remove(id(element)) - self.dict.pop(id(element)) + def remove(self, element: EltType) -> None: + self.set.remove(id(element)) + self.dict.pop(id(element)) diff --git a/edg/core/Link.py b/edg/core/Link.py index ecacc75e6..270e8f692 100644 --- a/edg/core/Link.py +++ b/edg/core/Link.py @@ -16,79 +16,83 @@ class LinkMeta(BaseBlockMeta): - def __new__(cls, *args: Any, **kwargs: Any) -> Any: - new_cls = super().__new__(cls, *args, **kwargs) + def __new__(cls, *args: Any, **kwargs: Any) -> Any: + new_cls = super().__new__(cls, *args, **kwargs) - if '__init__' in new_cls.__dict__: - orig_init = new_cls.__dict__['__init__'] - def wrapped_init(self: Any, *args: Any, **kwargs: Any) -> None: - builder_prev = builder.push_element(self) - try: - orig_init(self, *args, **kwargs) - finally: - builder.pop_to(builder_prev) + if "__init__" in new_cls.__dict__: + orig_init = new_cls.__dict__["__init__"] - new_cls.__init__ = functools.update_wrapper(wrapped_init, orig_init) + def wrapped_init(self: Any, *args: Any, **kwargs: Any) -> None: + builder_prev = builder.push_element(self) + try: + orig_init(self, *args, **kwargs) + finally: + builder.pop_to(builder_prev) - return new_cls + new_cls.__init__ = functools.update_wrapper(wrapped_init, orig_init) + + return new_cls @non_library class Link(BaseBlock, metaclass=LinkMeta): - def __init__(self) -> None: - super().__init__() - self.parent: Optional[Port] = None - - @override - def _def_to_proto(self) -> edgir.Link: - ref_map = self._create_ref_map() - - pb = edgir.Link() - self._populate_def_proto_block_base(pb) - self._populate_def_proto_param_init(pb, ref_map) - self._populate_def_proto_block_contents(pb, ref_map) - self._populate_def_proto_description(pb, ref_map) - # specifically ignore the port initializers - - # actually generate the links and connects - self._connects.finalize() - delegated_connects = self._all_delegated_connects() - for name, connect in self._connects.items_ordered(): - if connect in delegated_connects: - continue - connect_names_opt = [self._connects.name_of(c) for c in self._all_connects_of(connect)] - connect_names = [c for c in connect_names_opt if c is not None and not c.startswith('anon_')] - if len(connect_names) > 1: - raise UnconnectableError(f"Multiple names {connect_names} for connect") - elif len(connect_names) == 1: - name = connect_names[0] - - connect_elts = connect.make_connection() - assert isinstance(connect_elts, Connection.ConnectedLink) - - link_path = edgir.localpath_concat(edgir.LocalPath(), name) - edgir.add_pair(pb.links, name).lib_elem.target.name = connect_elts.link_type._static_def_name() - - assert not connect_elts.is_link_array - assert not connect_elts.bridged_connects - for idx, (self_port, link_port_path) in enumerate(connect_elts.link_connects): - constraint_pb = edgir.add_pair(pb.constraints, f"(export){name}_{idx}") - if isinstance(self_port, BaseVector): - assert isinstance(self_port, DerivedVector) - constraint_pb.exportedArray.exterior_port.map_extract.container.ref.CopyFrom(ref_map[self_port.base]) - constraint_pb.exportedArray.exterior_port.map_extract.path.steps.add().name = \ - self_port.target._name_from(self_port.base._get_elt_sample()) - constraint_pb.exportedArray.internal_block_port.ref.CopyFrom( - edgir.localpath_concat(link_path, link_port_path) - ) - else: - constraint_pb.exported.exterior_port.ref.CopyFrom(ref_map[self_port]) - constraint_pb.exported.internal_block_port.ref.CopyFrom( - edgir.localpath_concat(link_path, link_port_path) - ) - - return pb - - @override - def _elaborated_def_to_proto(self) -> edgir.Link: - return cast(edgir.Link, super()._elaborated_def_to_proto()) + def __init__(self) -> None: + super().__init__() + self.parent: Optional[Port] = None + + @override + def _def_to_proto(self) -> edgir.Link: + ref_map = self._create_ref_map() + + pb = edgir.Link() + self._populate_def_proto_block_base(pb) + self._populate_def_proto_param_init(pb, ref_map) + self._populate_def_proto_block_contents(pb, ref_map) + self._populate_def_proto_description(pb, ref_map) + # specifically ignore the port initializers + + # actually generate the links and connects + self._connects.finalize() + delegated_connects = self._all_delegated_connects() + for name, connect in self._connects.items_ordered(): + if connect in delegated_connects: + continue + connect_names_opt = [self._connects.name_of(c) for c in self._all_connects_of(connect)] + connect_names = [c for c in connect_names_opt if c is not None and not c.startswith("anon_")] + if len(connect_names) > 1: + raise UnconnectableError(f"Multiple names {connect_names} for connect") + elif len(connect_names) == 1: + name = connect_names[0] + + connect_elts = connect.make_connection() + assert isinstance(connect_elts, Connection.ConnectedLink) + + link_path = edgir.localpath_concat(edgir.LocalPath(), name) + edgir.add_pair(pb.links, name).lib_elem.target.name = connect_elts.link_type._static_def_name() + + assert not connect_elts.is_link_array + assert not connect_elts.bridged_connects + for idx, (self_port, link_port_path) in enumerate(connect_elts.link_connects): + constraint_pb = edgir.add_pair(pb.constraints, f"(export){name}_{idx}") + if isinstance(self_port, BaseVector): + assert isinstance(self_port, DerivedVector) + constraint_pb.exportedArray.exterior_port.map_extract.container.ref.CopyFrom( + ref_map[self_port.base] + ) + constraint_pb.exportedArray.exterior_port.map_extract.path.steps.add().name = ( + self_port.target._name_from(self_port.base._get_elt_sample()) + ) + constraint_pb.exportedArray.internal_block_port.ref.CopyFrom( + edgir.localpath_concat(link_path, link_port_path) + ) + else: + constraint_pb.exported.exterior_port.ref.CopyFrom(ref_map[self_port]) + constraint_pb.exported.internal_block_port.ref.CopyFrom( + edgir.localpath_concat(link_path, link_port_path) + ) + + return pb + + @override + def _elaborated_def_to_proto(self) -> edgir.Link: + return cast(edgir.Link, super()._elaborated_def_to_proto()) diff --git a/edg/core/MultiBiDict.py b/edg/core/MultiBiDict.py index 7a2bd215c..336a72d14 100644 --- a/edg/core/MultiBiDict.py +++ b/edg/core/MultiBiDict.py @@ -2,44 +2,45 @@ from typing import * -KeyType = TypeVar('KeyType', bound=Hashable) -ValueType = TypeVar('ValueType', bound=Hashable) +KeyType = TypeVar("KeyType", bound=Hashable) +ValueType = TypeVar("ValueType", bound=Hashable) + class MultiBiDict(Generic[KeyType, ValueType]): - def __init__(self) -> None: - self.dict: Dict[KeyType, Set[ValueType]] = {} - self.inverse_dict: Dict[ValueType, Set[KeyType]] = {} - - def add(self, key: KeyType, value: ValueType) -> None: - self.dict.setdefault(key, set()).add(value) - self.inverse_dict.setdefault(value, set()).add(key) - - def __contains__(self, key: KeyType) -> bool: - return len(self.dict.get(key, set())) > 0 - - def __getitem__(self, key: KeyType) -> ValueType: - value_set = self.dict.get(key, set()) - if len(value_set) != 1: - raise ValueError(f"MultiBiDict at key {key} has non-one value(s) {value_set}") - else: - return value_set.copy().pop() - - def contains_value(self, value: ValueType) -> bool: - return len(self.inverse_dict.get(value, set())) > 0 - - def get(self, key: KeyType) -> Set[ValueType]: - return self.dict.get(key, set()).copy() - - def get_by_value(self, value: ValueType) -> Set[KeyType]: # TODO better name - return self.inverse_dict.get(value, set()).copy() - - def get_only_by_value(self, value: ValueType) -> KeyType: # TODO better name - key_set = self.inverse_dict.get(value, set()) - if len(key_set) != 1: - raise ValueError(f"MultiBiDict at value {value} has non-one key(s) {key_set}") - else: - return key_set.copy().pop() - - def clear(self) -> None: - self.dict = {} - self.inverse_dict = {} + def __init__(self) -> None: + self.dict: Dict[KeyType, Set[ValueType]] = {} + self.inverse_dict: Dict[ValueType, Set[KeyType]] = {} + + def add(self, key: KeyType, value: ValueType) -> None: + self.dict.setdefault(key, set()).add(value) + self.inverse_dict.setdefault(value, set()).add(key) + + def __contains__(self, key: KeyType) -> bool: + return len(self.dict.get(key, set())) > 0 + + def __getitem__(self, key: KeyType) -> ValueType: + value_set = self.dict.get(key, set()) + if len(value_set) != 1: + raise ValueError(f"MultiBiDict at key {key} has non-one value(s) {value_set}") + else: + return value_set.copy().pop() + + def contains_value(self, value: ValueType) -> bool: + return len(self.inverse_dict.get(value, set())) > 0 + + def get(self, key: KeyType) -> Set[ValueType]: + return self.dict.get(key, set()).copy() + + def get_by_value(self, value: ValueType) -> Set[KeyType]: # TODO better name + return self.inverse_dict.get(value, set()).copy() + + def get_only_by_value(self, value: ValueType) -> KeyType: # TODO better name + key_set = self.inverse_dict.get(value, set()) + if len(key_set) != 1: + raise ValueError(f"MultiBiDict at value {value} has non-one key(s) {key_set}") + else: + return key_set.copy().pop() + + def clear(self) -> None: + self.dict = {} + self.inverse_dict = {} diff --git a/edg/core/MultipackBlock.py b/edg/core/MultipackBlock.py index aa976e8c7..0b615504b 100644 --- a/edg/core/MultipackBlock.py +++ b/edg/core/MultipackBlock.py @@ -17,78 +17,87 @@ class PackedBlockAllocate(NamedTuple): - parent: PackedBlockArray - suggested_name: Optional[str] + parent: PackedBlockArray + suggested_name: Optional[str] + + +ArrayPortType = TypeVar("ArrayPortType", covariant=True, bound=Port, default=Port) -ArrayPortType = TypeVar('ArrayPortType', covariant=True, bound=Port, default=Port) class PackedBlockPortArray(Generic[ArrayPortType]): - def __init__(self, parent: PackedBlockArray, port: ArrayPortType): - self.parent = parent - self.port = port + def __init__(self, parent: PackedBlockArray, port: ArrayPortType): + self.parent = parent + self.port = port + + +ArrayParamType = TypeVar("ArrayParamType", covariant=True, bound=ConstraintExpr, default=ConstraintExpr) -ArrayParamType = TypeVar('ArrayParamType', covariant=True, bound=ConstraintExpr, default=ConstraintExpr) class PackedBlockParamArray(Generic[ArrayParamType]): # an array of parameters from an array of parts - def __init__(self, parent: PackedBlockArray, param: ArrayParamType): - self.parent = parent - self.param = param + def __init__(self, parent: PackedBlockArray, param: ArrayParamType): + self.parent = parent + self.param = param class PackedBlockParam(NamedTuple): # a parameter replicated from an array of blocks - parent: PackedBlockArray - param: ConstraintExpr + parent: PackedBlockArray + param: ConstraintExpr + + +PackedBlockElementType = TypeVar("PackedBlockElementType", covariant=True, bound=Block, default=Block) -PackedBlockElementType = TypeVar('PackedBlockElementType', covariant=True, bound=Block, default=Block) class PackedBlockArray(Generic[PackedBlockElementType]): - """A container "block" (for multipack packing only) for an arbitrary-length array of Blocks. - This is meant to be analogous to Vector (port arrays), though there isn't an use case for this in general - (non-multipack) core infrastructure yet.""" - def __init__(self, tpe: PackedBlockElementType): - self._tpe = tpe - self._elt_sample: Optional[PackedBlockElementType] = None # inner facing only - self._parent: Optional[Block] = None - self._allocates: List[Tuple[Optional[str], Block]] = [] # outer facing only, to track allocate for ref_map - - def _bind(self, parent: Block) -> PackedBlockArray[PackedBlockElementType]: - clone = PackedBlockArray(self._tpe) - clone._parent = parent - clone._elt_sample = self._tpe._bind(parent) - return clone - - @deprecated(reason="renamed to request") - def allocate(self, suggested_name: Optional[str] = None) -> PackedBlockAllocate: - return self.request(suggested_name) - - def request(self, suggested_name: Optional[str] = None) -> PackedBlockAllocate: - """External API, to request a new instance for an array element / packed part.""" - assert self._parent is not None, "no parent set, cannot allocate" - return PackedBlockAllocate(self, suggested_name) - - # TODO does this need to return a narrower type? - # TODO would it be useful to return a proper Vector type, instead of this special PackedBlockPortArray? - PortType = TypeVar('PortType', bound=Port) - def ports_array(self, selector: Callable[[PackedBlockElementType], PortType]) -> PackedBlockPortArray[PortType]: - """Return an array of ports, packed from the selected port of each array element.""" - assert self._elt_sample is not None, "no sample element set, cannot allocate" - return PackedBlockPortArray(self, selector(self._elt_sample)) - - # TODO does this need to return a narrower type? - # TODO would it be useful to return a proper ConstraintExpr type, instead of this special PackedBlockParamArray? - ParamType = TypeVar('ParamType', bound=ConstraintExpr) - def params_array(self, selector: Callable[[PackedBlockElementType], ParamType]) -> PackedBlockParamArray[ParamType]: - """Return an array of params, packed from the selected param of each array element.""" - assert self._elt_sample is not None, "no sample element set, cannot allocate" - return PackedBlockParamArray(self, selector(self._elt_sample)) - - # TODO does this need to return a narrower type? - # TODO would it be useful to return a proper ConstraintExpr type, instead of this special PackedBlockParamArray? - def params(self, selector: Callable[[PackedBlockElementType], ConstraintExpr]) -> PackedBlockParam: - """Return the selected param on each array element. Only valid for unpacked assign, where the assign - is replicated to the selected param on each packed block.""" - assert self._elt_sample is not None, "no sample element set, cannot allocate" - return PackedBlockParam(self, selector(self._elt_sample)) + """A container "block" (for multipack packing only) for an arbitrary-length array of Blocks. + This is meant to be analogous to Vector (port arrays), though there isn't an use case for this in general + (non-multipack) core infrastructure yet.""" + + def __init__(self, tpe: PackedBlockElementType): + self._tpe = tpe + self._elt_sample: Optional[PackedBlockElementType] = None # inner facing only + self._parent: Optional[Block] = None + self._allocates: List[Tuple[Optional[str], Block]] = [] # outer facing only, to track allocate for ref_map + + def _bind(self, parent: Block) -> PackedBlockArray[PackedBlockElementType]: + clone = PackedBlockArray(self._tpe) + clone._parent = parent + clone._elt_sample = self._tpe._bind(parent) + return clone + + @deprecated(reason="renamed to request") + def allocate(self, suggested_name: Optional[str] = None) -> PackedBlockAllocate: + return self.request(suggested_name) + + def request(self, suggested_name: Optional[str] = None) -> PackedBlockAllocate: + """External API, to request a new instance for an array element / packed part.""" + assert self._parent is not None, "no parent set, cannot allocate" + return PackedBlockAllocate(self, suggested_name) + + # TODO does this need to return a narrower type? + # TODO would it be useful to return a proper Vector type, instead of this special PackedBlockPortArray? + PortType = TypeVar("PortType", bound=Port) + + def ports_array(self, selector: Callable[[PackedBlockElementType], PortType]) -> PackedBlockPortArray[PortType]: + """Return an array of ports, packed from the selected port of each array element.""" + assert self._elt_sample is not None, "no sample element set, cannot allocate" + return PackedBlockPortArray(self, selector(self._elt_sample)) + + # TODO does this need to return a narrower type? + # TODO would it be useful to return a proper ConstraintExpr type, instead of this special PackedBlockParamArray? + ParamType = TypeVar("ParamType", bound=ConstraintExpr) + + def params_array(self, selector: Callable[[PackedBlockElementType], ParamType]) -> PackedBlockParamArray[ParamType]: + """Return an array of params, packed from the selected param of each array element.""" + assert self._elt_sample is not None, "no sample element set, cannot allocate" + return PackedBlockParamArray(self, selector(self._elt_sample)) + + # TODO does this need to return a narrower type? + # TODO would it be useful to return a proper ConstraintExpr type, instead of this special PackedBlockParamArray? + def params(self, selector: Callable[[PackedBlockElementType], ConstraintExpr]) -> PackedBlockParam: + """Return the selected param on each array element. Only valid for unpacked assign, where the assign + is replicated to the selected param on each packed block.""" + assert self._elt_sample is not None, "no sample element set, cannot allocate" + return PackedBlockParam(self, selector(self._elt_sample)) # These are all internal-ish APIs (within MultipackBlock - NOT in DesignTop) @@ -99,166 +108,180 @@ def params(self, selector: Callable[[PackedBlockElementType], ConstraintExpr]) - class MultipackPackingRule(NamedTuple): - tunnel_exports: IdentityDict[BasePort, PackedPortTypes] # exterior port -> packed block port - tunnel_assigns: IdentityDict[ConstraintExpr, PackedParamTypes] # my param -> packed block param - tunnel_unpack_assigns: IdentityDict[ConstraintExpr, UnpackedParamTypes] # packed block param -> my param + tunnel_exports: IdentityDict[BasePort, PackedPortTypes] # exterior port -> packed block port + tunnel_assigns: IdentityDict[ConstraintExpr, PackedParamTypes] # my param -> packed block param + tunnel_unpack_assigns: IdentityDict[ConstraintExpr, UnpackedParamTypes] # packed block param -> my param @non_library class MultipackBlock(Block): - """A block that represents a packed single device that is composed of other blocks - for example, - a dual-pack opamp or quad-pack (or n-pack) resistor array. - - This allows the single element blocks (eg, single opamp or single resistor) to be packed into these as - a type of cross-hierarchy optimization, specified at the top level. - - While this block can be directly instantiated (and will do what you think) anywhere, using this in libraries - is not recommended - instead use the single-element blocks (to make libraries as general as possible, without - premature optimization), then specify the multipack optimization at the top-level. - - This block contains both the implementation (like any normal block - for example, a dual-pack opamp would - implement the application circuit, containing sub-blocks for both the decoupling cap and the chip) and the - packing definition (specific to this class - but does not contribute to the block implementation). - """ - def __init__(self) -> None: - super().__init__() - self._packed_blocks: SubElementDict[PackedBlockTypes] = self.manager.new_dict((Block, PackedBlockArray)) - # TODO should these be defined in terms of Refs? - # packed block -> (exterior port -> packed block port) - self._packed_connects_by_packed_block = IdentityDict[PackedBlockTypes, IdentityDict[BasePort, PackedPortTypes]]() - # packed block -> (self param -> packed param) (reverse of assign direction) - self._packed_assigns_by_packed_block = IdentityDict[PackedBlockTypes, IdentityDict[ConstraintExpr, PackedParamTypes]]() - # packed block -> (self param -> packed param) - self._unpacked_assigns_by_packed_block = IdentityDict[PackedBlockTypes, IdentityDict[ConstraintExpr, UnpackedParamTypes]]() - - PackedPartType = TypeVar('PackedPartType', bound=Union[Block, PackedBlockArray]) - def PackedPart(self, tpe: PackedPartType) -> PackedPartType: - """Adds a block type that can be packed into this block. - The block is a "virtual block" that will not appear in the design tree.""" - if isinstance(tpe, BlockPrototype): - tpe_cls = tpe._tpe - else: - tpe_cls = tpe.__class__ - - if not issubclass(tpe_cls, (Block, PackedBlockArray)): - raise EdgTypeError(f"PackedPart(...) param", tpe, Block) - if self._elaboration_state != BlockElaborationState.init: - raise BlockDefinitionError(type(self), "can only define multipack in init") - - elt = tpe._bind(self) # TODO: does this actually need to be bound? - self._packed_blocks.register(elt) - self._packed_connects_by_packed_block[elt] = IdentityDict[BasePort, PackedPortTypes]() - self._packed_assigns_by_packed_block[elt] = IdentityDict[ConstraintExpr, PackedParamTypes]() - self._unpacked_assigns_by_packed_block[elt] = IdentityDict[ConstraintExpr, UnpackedParamTypes]() - - return elt # type: ignore - - def packed_connect(self, exterior_port: BasePort, packed_port: PackedPortTypes) -> None: - """Defines a packing rule specified as a virtual connection between an exterior port and a PackedBlock port.""" - if self._elaboration_state != BlockElaborationState.init: - raise BlockDefinitionError(type(self), "can only define multipack in init") - if isinstance(packed_port, Port): - assert type(exterior_port) == type(packed_port), "packed_connect ports must be of the same type" - block_parent = packed_port._block_parent() - assert isinstance(block_parent, Block) - self._packed_connects_by_packed_block[block_parent][exterior_port] = packed_port - elif isinstance(packed_port, PackedBlockPortArray): - assert isinstance(exterior_port, Vector), "can only connect vector from packed port array" - assert type(exterior_port._elt_sample) == type(packed_port.port), "packed_connect ports must be of the same type" - self._packed_connects_by_packed_block[packed_port.parent][exterior_port] = packed_port - else: - raise TypeError() - - PackedPortType = TypeVar('PackedPortType', bound=Port) - @overload - def PackedExport(self, packed_port: PackedPortType, *, optional: bool = False) -> PackedPortType: ... - @overload - def PackedExport(self, packed_port: PackedBlockPortArray[PackedPortType], *, optional: bool = False) -> Vector[PackedPortType]: ... - - def PackedExport(self, packed_port: PackedPortTypes, *, optional: bool = False) -> BasePort: - """Defines a Port in this block, by exporting a port from a packed part or packed part array. - Like self.Export(...), combines self.Port(...) with self.packed_connect(...).""" - if isinstance(packed_port, Port): - new_port: BasePort = self.Port(type(packed_port).empty(), optional=optional) - elif isinstance(packed_port, PackedBlockPortArray): - new_port = self.Port(Vector(type(packed_port.port).empty()), optional=optional) - else: - raise TypeError() - self.packed_connect(new_port, packed_port) # all checks happen here - return new_port - - def packed_assign(self, self_param: ConstraintExpr, packed_param: PackedParamTypes) -> None: - """Defines a packing rule assigning my parameter from a PackedBlock parameter. - IMPORTANT: for packed arrays, no ordering on elements is guaranteed, and must be treated as an unordered set.""" - if self._elaboration_state != BlockElaborationState.init: - raise BlockDefinitionError(type(self), "can only define multipack in init") - if isinstance(packed_param, ConstraintExpr): - assert type(self_param) == type(packed_param), "packed_assign parameters must be of the same type" - block_parent = packed_param._context - assert isinstance(block_parent, Block) - self._packed_assigns_by_packed_block[block_parent][self_param] = packed_param - elif isinstance(packed_param, PackedBlockParamArray): - assert isinstance(self_param, ArrayExpr), "can only assign array expr from packed param array" - assert self_param._elt_type == type(packed_param.param), "packed_assign parameters must be of the same type" - self._packed_assigns_by_packed_block[packed_param.parent][self_param] = packed_param - else: - raise TypeError() - - PackedParamType = TypeVar('PackedParamType', bound=ConstraintExpr) - @overload - def PackedParameter(self, packed_param: PackedParamType) -> PackedParamType: ... - @overload - def PackedParameter(self, packed_param: PackedBlockParamArray[BoolExpr]) -> ArrayBoolExpr: ... - @overload - def PackedParameter(self, packed_param: PackedBlockParamArray[IntExpr]) -> ArrayIntExpr: ... - @overload - def PackedParameter(self, packed_param: PackedBlockParamArray[FloatExpr]) -> ArrayFloatExpr: ... - @overload - def PackedParameter(self, packed_param: PackedBlockParamArray[RangeExpr]) -> ArrayRangeExpr: ... - @overload - def PackedParameter(self, packed_param: PackedBlockParamArray[StringExpr]) -> ArrayStringExpr: ... - - def PackedParameter(self, packed_param: PackedParamTypes) -> ConstraintExpr: - """Defines a Parameter in this block, by exporting a parameter from a packed part or packed part array. - Combines self.Parameter(...) with self.packed_assign(...), and additionally compatible with generators - where self.Parameter(...) would error out.""" - if isinstance(packed_param, ConstraintExpr): - new_param = type(packed_param)()._bind(InitParamBinding(self)) - elif isinstance(packed_param, PackedBlockParamArray): - new_param = ArrayExpr.array_of_elt(packed_param.param)._bind(InitParamBinding(self)) - else: - raise TypeError() - self.packed_assign(new_param, packed_param) - self._parameters.register(new_param) - return new_param - - def unpacked_assign(self, packed_param: UnpackedParamTypes, self_param: ConstraintExpr) -> None: - """Defines an (un)packing rule assigning a Packed parameter from my parameter (reverse of packed_assign). - Only direct parameter-to-parameter assignment allowed, even for packed block arrays, + """A block that represents a packed single device that is composed of other blocks - for example, + a dual-pack opamp or quad-pack (or n-pack) resistor array. + + This allows the single element blocks (eg, single opamp or single resistor) to be packed into these as + a type of cross-hierarchy optimization, specified at the top level. + + While this block can be directly instantiated (and will do what you think) anywhere, using this in libraries + is not recommended - instead use the single-element blocks (to make libraries as general as possible, without + premature optimization), then specify the multipack optimization at the top-level. + + This block contains both the implementation (like any normal block - for example, a dual-pack opamp would + implement the application circuit, containing sub-blocks for both the decoupling cap and the chip) and the + packing definition (specific to this class - but does not contribute to the block implementation). """ - if self._elaboration_state != BlockElaborationState.init: - raise BlockDefinitionError(type(self), "can only define multipack in init") - if isinstance(packed_param, ConstraintExpr): - assert type(packed_param) == type(self_param), "unpacked_assign parameters must be of the same type" - block_parent = packed_param._context - assert isinstance(block_parent, Block) - self._unpacked_assigns_by_packed_block[block_parent][self_param] = packed_param - elif isinstance(packed_param, PackedBlockParam): - assert type(packed_param.param) == type(self_param), "unpacked_assign parameters must be of the same type" - self._unpacked_assigns_by_packed_block[packed_param.parent][self_param] = packed_param - else: - raise TypeError() - - def _get_block_packing_rule(self, packed_part: Union[Block, PackedBlockAllocate]) -> MultipackPackingRule: - """Internal API, returns the packing rules (tunnel exports and assigns) for a constituent PackedPart.""" - self._packed_blocks.finalize() - if isinstance(packed_part, PackedBlockAllocate): - packed_block: PackedBlockTypes = packed_part.parent - else: - packed_block = packed_part - - return MultipackPackingRule( - self._packed_connects_by_packed_block[packed_block], - self._packed_assigns_by_packed_block[packed_block], - self._unpacked_assigns_by_packed_block[packed_block] - ) + + def __init__(self) -> None: + super().__init__() + self._packed_blocks: SubElementDict[PackedBlockTypes] = self.manager.new_dict((Block, PackedBlockArray)) + # TODO should these be defined in terms of Refs? + # packed block -> (exterior port -> packed block port) + self._packed_connects_by_packed_block = IdentityDict[ + PackedBlockTypes, IdentityDict[BasePort, PackedPortTypes] + ]() + # packed block -> (self param -> packed param) (reverse of assign direction) + self._packed_assigns_by_packed_block = IdentityDict[ + PackedBlockTypes, IdentityDict[ConstraintExpr, PackedParamTypes] + ]() + # packed block -> (self param -> packed param) + self._unpacked_assigns_by_packed_block = IdentityDict[ + PackedBlockTypes, IdentityDict[ConstraintExpr, UnpackedParamTypes] + ]() + + PackedPartType = TypeVar("PackedPartType", bound=Union[Block, PackedBlockArray]) + + def PackedPart(self, tpe: PackedPartType) -> PackedPartType: + """Adds a block type that can be packed into this block. + The block is a "virtual block" that will not appear in the design tree.""" + if isinstance(tpe, BlockPrototype): + tpe_cls = tpe._tpe + else: + tpe_cls = tpe.__class__ + + if not issubclass(tpe_cls, (Block, PackedBlockArray)): + raise EdgTypeError(f"PackedPart(...) param", tpe, Block) + if self._elaboration_state != BlockElaborationState.init: + raise BlockDefinitionError(type(self), "can only define multipack in init") + + elt = tpe._bind(self) # TODO: does this actually need to be bound? + self._packed_blocks.register(elt) + self._packed_connects_by_packed_block[elt] = IdentityDict[BasePort, PackedPortTypes]() + self._packed_assigns_by_packed_block[elt] = IdentityDict[ConstraintExpr, PackedParamTypes]() + self._unpacked_assigns_by_packed_block[elt] = IdentityDict[ConstraintExpr, UnpackedParamTypes]() + + return elt # type: ignore + + def packed_connect(self, exterior_port: BasePort, packed_port: PackedPortTypes) -> None: + """Defines a packing rule specified as a virtual connection between an exterior port and a PackedBlock port.""" + if self._elaboration_state != BlockElaborationState.init: + raise BlockDefinitionError(type(self), "can only define multipack in init") + if isinstance(packed_port, Port): + assert type(exterior_port) == type(packed_port), "packed_connect ports must be of the same type" + block_parent = packed_port._block_parent() + assert isinstance(block_parent, Block) + self._packed_connects_by_packed_block[block_parent][exterior_port] = packed_port + elif isinstance(packed_port, PackedBlockPortArray): + assert isinstance(exterior_port, Vector), "can only connect vector from packed port array" + assert type(exterior_port._elt_sample) == type( + packed_port.port + ), "packed_connect ports must be of the same type" + self._packed_connects_by_packed_block[packed_port.parent][exterior_port] = packed_port + else: + raise TypeError() + + PackedPortType = TypeVar("PackedPortType", bound=Port) + + @overload + def PackedExport(self, packed_port: PackedPortType, *, optional: bool = False) -> PackedPortType: ... + @overload + def PackedExport( + self, packed_port: PackedBlockPortArray[PackedPortType], *, optional: bool = False + ) -> Vector[PackedPortType]: ... + + def PackedExport(self, packed_port: PackedPortTypes, *, optional: bool = False) -> BasePort: + """Defines a Port in this block, by exporting a port from a packed part or packed part array. + Like self.Export(...), combines self.Port(...) with self.packed_connect(...).""" + if isinstance(packed_port, Port): + new_port: BasePort = self.Port(type(packed_port).empty(), optional=optional) + elif isinstance(packed_port, PackedBlockPortArray): + new_port = self.Port(Vector(type(packed_port.port).empty()), optional=optional) + else: + raise TypeError() + self.packed_connect(new_port, packed_port) # all checks happen here + return new_port + + def packed_assign(self, self_param: ConstraintExpr, packed_param: PackedParamTypes) -> None: + """Defines a packing rule assigning my parameter from a PackedBlock parameter. + IMPORTANT: for packed arrays, no ordering on elements is guaranteed, and must be treated as an unordered set.""" + if self._elaboration_state != BlockElaborationState.init: + raise BlockDefinitionError(type(self), "can only define multipack in init") + if isinstance(packed_param, ConstraintExpr): + assert type(self_param) == type(packed_param), "packed_assign parameters must be of the same type" + block_parent = packed_param._context + assert isinstance(block_parent, Block) + self._packed_assigns_by_packed_block[block_parent][self_param] = packed_param + elif isinstance(packed_param, PackedBlockParamArray): + assert isinstance(self_param, ArrayExpr), "can only assign array expr from packed param array" + assert self_param._elt_type == type(packed_param.param), "packed_assign parameters must be of the same type" + self._packed_assigns_by_packed_block[packed_param.parent][self_param] = packed_param + else: + raise TypeError() + + PackedParamType = TypeVar("PackedParamType", bound=ConstraintExpr) + + @overload + def PackedParameter(self, packed_param: PackedParamType) -> PackedParamType: ... + @overload + def PackedParameter(self, packed_param: PackedBlockParamArray[BoolExpr]) -> ArrayBoolExpr: ... + @overload + def PackedParameter(self, packed_param: PackedBlockParamArray[IntExpr]) -> ArrayIntExpr: ... + @overload + def PackedParameter(self, packed_param: PackedBlockParamArray[FloatExpr]) -> ArrayFloatExpr: ... + @overload + def PackedParameter(self, packed_param: PackedBlockParamArray[RangeExpr]) -> ArrayRangeExpr: ... + @overload + def PackedParameter(self, packed_param: PackedBlockParamArray[StringExpr]) -> ArrayStringExpr: ... + + def PackedParameter(self, packed_param: PackedParamTypes) -> ConstraintExpr: + """Defines a Parameter in this block, by exporting a parameter from a packed part or packed part array. + Combines self.Parameter(...) with self.packed_assign(...), and additionally compatible with generators + where self.Parameter(...) would error out.""" + if isinstance(packed_param, ConstraintExpr): + new_param = type(packed_param)()._bind(InitParamBinding(self)) + elif isinstance(packed_param, PackedBlockParamArray): + new_param = ArrayExpr.array_of_elt(packed_param.param)._bind(InitParamBinding(self)) + else: + raise TypeError() + self.packed_assign(new_param, packed_param) + self._parameters.register(new_param) + return new_param + + def unpacked_assign(self, packed_param: UnpackedParamTypes, self_param: ConstraintExpr) -> None: + """Defines an (un)packing rule assigning a Packed parameter from my parameter (reverse of packed_assign). + Only direct parameter-to-parameter assignment allowed, even for packed block arrays, + """ + if self._elaboration_state != BlockElaborationState.init: + raise BlockDefinitionError(type(self), "can only define multipack in init") + if isinstance(packed_param, ConstraintExpr): + assert type(packed_param) == type(self_param), "unpacked_assign parameters must be of the same type" + block_parent = packed_param._context + assert isinstance(block_parent, Block) + self._unpacked_assigns_by_packed_block[block_parent][self_param] = packed_param + elif isinstance(packed_param, PackedBlockParam): + assert type(packed_param.param) == type(self_param), "unpacked_assign parameters must be of the same type" + self._unpacked_assigns_by_packed_block[packed_param.parent][self_param] = packed_param + else: + raise TypeError() + + def _get_block_packing_rule(self, packed_part: Union[Block, PackedBlockAllocate]) -> MultipackPackingRule: + """Internal API, returns the packing rules (tunnel exports and assigns) for a constituent PackedPart.""" + self._packed_blocks.finalize() + if isinstance(packed_part, PackedBlockAllocate): + packed_block: PackedBlockTypes = packed_part.parent + else: + packed_block = packed_part + + return MultipackPackingRule( + self._packed_connects_by_packed_block[packed_block], + self._packed_assigns_by_packed_block[packed_block], + self._unpacked_assigns_by_packed_block[packed_block], + ) diff --git a/edg/core/PortBlocks.py b/edg/core/PortBlocks.py index 605ef23e6..fcb21012f 100644 --- a/edg/core/PortBlocks.py +++ b/edg/core/PortBlocks.py @@ -10,54 +10,66 @@ @abstract_block class PortBridge(InternalBlock, Block): - """Defines rules for connecting the internal port of a hierarchy block to a link. - Only needed if the internal port connects to an internal link and is NOT a one-to-one forwarding port. - - Note: this is a regular block in the IR, but is conceptually different and inferred in the frontend for ease-of-use. - - Example: a power sink internal port can connect to one power sink port on an internal block without a port bridge, - but requires a port bridge to connect to a power link that serves multiple power sinks on internal blocks. - """ - def __init__(self) -> None: - super().__init__() - # TODO these should be type Port[Any], but that seems to break type inference - self.outer_port: Any - self.inner_link: Any - - @override - def __setattr__(self, name: str, value: Any) -> None: - if isinstance(value, Port): - assert name == '_parent' or name == "outer_port" or name == "inner_link", \ - "PortBridge can only have outer_port or inner_link ports, got %s" % name - super().__setattr__(name, value) - - T = TypeVar('T', bound=BasePort) - @override - def Port(self, tpe: T, *args: Any, **kwargs: Any) -> T: - assert 'optional' not in kwargs, f"Ports in PortBridge are optional by default, required should be set by enclosing block, in {kwargs}" - return super().Port(tpe, *args, optional=True, **kwargs) - - -AdapterDstType = TypeVar('AdapterDstType', covariant=True, bound=Port, default=Port) + """Defines rules for connecting the internal port of a hierarchy block to a link. + Only needed if the internal port connects to an internal link and is NOT a one-to-one forwarding port. + + Note: this is a regular block in the IR, but is conceptually different and inferred in the frontend for ease-of-use. + + Example: a power sink internal port can connect to one power sink port on an internal block without a port bridge, + but requires a port bridge to connect to a power link that serves multiple power sinks on internal blocks. + """ + + def __init__(self) -> None: + super().__init__() + # TODO these should be type Port[Any], but that seems to break type inference + self.outer_port: Any + self.inner_link: Any + + @override + def __setattr__(self, name: str, value: Any) -> None: + if isinstance(value, Port): + assert name == "_parent" or name == "outer_port" or name == "inner_link", ( + "PortBridge can only have outer_port or inner_link ports, got %s" % name + ) + super().__setattr__(name, value) + + T = TypeVar("T", bound=BasePort) + + @override + def Port(self, tpe: T, *args: Any, **kwargs: Any) -> T: + assert ( + "optional" not in kwargs + ), f"Ports in PortBridge are optional by default, required should be set by enclosing block, in {kwargs}" + return super().Port(tpe, *args, optional=True, **kwargs) + + +AdapterDstType = TypeVar("AdapterDstType", covariant=True, bound=Port, default=Port) + + @abstract_block class PortAdapter(InternalBlock, Block, Generic[AdapterDstType]): - """Defines an adapter from one port type to another port type. This behaves as a normal block, and both the src and - dst are connected with normal connect semantics. Should only be inferred on internal block ports.""" - def __init__(self) -> None: - super().__init__() - # TODO these should be type Port[Any], but that seems to break type inference - self.src: Any - self.dst: AdapterDstType - - @override - def __setattr__(self, name: str, value: Any) -> None: - if isinstance(value, Port): - assert name == '_parent' or name == "src" or name == "dst", \ - "PortAdapter can only have src or dst ports, got %s" % name - super().__setattr__(name, value) - - T = TypeVar('T', bound=BasePort) - @override - def Port(self, tpe: T, *args: Any, **kwargs: Any) -> T: - assert 'optional' not in kwargs, "Ports in PortBridge are optional by default, required should be set by enclosing block" - return super().Port(tpe, *args, optional=True, **kwargs) + """Defines an adapter from one port type to another port type. This behaves as a normal block, and both the src and + dst are connected with normal connect semantics. Should only be inferred on internal block ports.""" + + def __init__(self) -> None: + super().__init__() + # TODO these should be type Port[Any], but that seems to break type inference + self.src: Any + self.dst: AdapterDstType + + @override + def __setattr__(self, name: str, value: Any) -> None: + if isinstance(value, Port): + assert name == "_parent" or name == "src" or name == "dst", ( + "PortAdapter can only have src or dst ports, got %s" % name + ) + super().__setattr__(name, value) + + T = TypeVar("T", bound=BasePort) + + @override + def Port(self, tpe: T, *args: Any, **kwargs: Any) -> T: + assert ( + "optional" not in kwargs + ), "Ports in PortBridge are optional by default, required should be set by enclosing block" + return super().Port(tpe, *args, optional=True, **kwargs) diff --git a/edg/core/PortTag.py b/edg/core/PortTag.py index b4333ec57..e97e7faac 100644 --- a/edg/core/PortTag.py +++ b/edg/core/PortTag.py @@ -5,10 +5,12 @@ class PortTag: - def __init__(self, tpe: Type[BasePort]) -> None: - self.port_tpe = tpe + def __init__(self, tpe: Type[BasePort]) -> None: + self.port_tpe = tpe -Input = PortTag(Port) # basic untyped tag for implicit dataflow inputs (including voltage sources); for only voltages, this would be the one with the largest magnitude +Input = PortTag( + Port +) # basic untyped tag for implicit dataflow inputs (including voltage sources); for only voltages, this would be the one with the largest magnitude Output = PortTag(Port) # basic untyped tag for implicit dataflow outputs InOut = PortTag(Port) # basic untyped tag for implicit dataflow inout, including pullup/pulldowns diff --git a/edg/core/Ports.py b/edg/core/Ports.py index e582e7d31..55142a83b 100644 --- a/edg/core/Ports.py +++ b/edg/core/Ports.py @@ -15,353 +15,367 @@ from .. import edgir if TYPE_CHECKING: - from .Blocks import BaseBlock - from .Link import Link - from .PortBlocks import PortBridge, PortAdapter + from .Blocks import BaseBlock + from .Link import Link + from .PortBlocks import PortBridge, PortAdapter class InitializerContextMeta(type): - @override - def __call__(cls, *args: Any, **kwargs: Any) -> Any: - """Hook on construction to store some metadata about its creation. - This hooks the top-level __init__ only.""" - obj = type.__call__(cls, *args, **kwargs) - obj._initializer_args = (args, kwargs) # stores args so it is clone-able - return obj + @override + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + """Hook on construction to store some metadata about its creation. + This hooks the top-level __init__ only.""" + obj = type.__call__(cls, *args, **kwargs) + obj._initializer_args = (args, kwargs) # stores args so it is clone-able + return obj + + +PortParentTypes = Union["BaseContainerPort", "BaseBlock"] -PortParentTypes = Union['BaseContainerPort', 'BaseBlock'] @non_library class BasePort(HasMetadata, metaclass=InitializerContextMeta): - SelfType = TypeVar('SelfType', bound='BasePort') + SelfType = TypeVar("SelfType", bound="BasePort") + + def __init__(self) -> None: + """Abstract Base Class for ports""" + self._parent: Optional[PortParentTypes] # refined from Optional[Refable] in base LibraryElement + self._block_context: Optional["Refable"] # set by metaclass, as lexical scope available pre-binding + self._initializer_args: Tuple[Tuple[Any, ...], Dict[str, Any]] # set by metaclass + self._block_context = builder.get_enclosing_block() + + super().__init__() + + def _block_parent(self) -> Optional[BaseBlock]: + from .Blocks import BaseBlock + + if isinstance(self._parent, BasePort): + return self._parent._block_parent() + elif isinstance(self._parent, BaseBlock): + return self._parent + elif self._parent is None: + return None + else: + raise ValueError(f"Unknown parent type {self._parent}") + + @abstractmethod + @override + def _def_to_proto(self) -> edgir.PortTypes: # TODO: this might not be valid for Vector types? + raise NotImplementedError + + @abstractmethod + def _type_of(self) -> Hashable: ... + + @abstractmethod + def _populate_portlike_proto(self, pb: edgir.PortLike) -> None: + """Populates the proto of an instance of this object""" + raise NotImplementedError + + def _bind_in_place(self, parent: PortParentTypes) -> None: + self._parent = parent + + def _clone(self: SelfType) -> SelfType: + """Returns a fresh clone of this object, with fresh references but preserving user-specified state like + parameter initializers.""" + assert self._parent is None, "can't clone bound block" + # TODO: this might be more efficient (but trickier) with copy.copy + cloned = type(self)(*self._initializer_args[0], **self._initializer_args[1]) + cloned._cloned_from(self) + return cloned + + def _cloned_from(self: SelfType, other: SelfType) -> None: + """Copies user-specified initializers from other.""" + pass + + def _bind(self: SelfType, parent: PortParentTypes) -> SelfType: + """Returns a clone of this object with the specified binding. This object must be unbound.""" + assert ( + builder.get_enclosing_block() is self._block_context + ), f"can't clone to different context, {builder.get_enclosing_block()} -> {self._block_context}" + clone = self._clone() + clone._bind_in_place(parent) + return clone + + def _is_bound(self) -> bool: + def impl(elt: Optional[PortParentTypes]) -> bool: + if elt is None: + return False + elif isinstance(elt, BasePort): + return impl(elt._parent) + else: + return True + + return impl(self._parent) + + @abstractmethod + def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: + """Returns all the initializers of contained parameters, as tuples of (parameter, path, initializer value). + Parameters without initializers are skipped.""" + raise NotImplementedError - def __init__(self) -> None: - """Abstract Base Class for ports""" - self._parent: Optional[PortParentTypes] # refined from Optional[Refable] in base LibraryElement - self._block_context: Optional["Refable"] # set by metaclass, as lexical scope available pre-binding - self._initializer_args: Tuple[Tuple[Any, ...], Dict[str, Any]] # set by metaclass - self._block_context = builder.get_enclosing_block() - super().__init__() - - def _block_parent(self) -> Optional[BaseBlock]: - from .Blocks import BaseBlock - if isinstance(self._parent, BasePort): - return self._parent._block_parent() - elif isinstance(self._parent, BaseBlock): - return self._parent - elif self._parent is None: - return None - else: - raise ValueError(f"Unknown parent type {self._parent}") - - @abstractmethod - @override - def _def_to_proto(self) -> edgir.PortTypes: # TODO: this might not be valid for Vector types? - raise NotImplementedError - - @abstractmethod - def _type_of(self) -> Hashable: ... - - @abstractmethod - def _populate_portlike_proto(self, pb: edgir.PortLike) -> None: - """Populates the proto of an instance of this object""" - raise NotImplementedError - - def _bind_in_place(self, parent: PortParentTypes) -> None: - self._parent = parent - - def _clone(self: SelfType) -> SelfType: - """Returns a fresh clone of this object, with fresh references but preserving user-specified state like - parameter initializers.""" - assert self._parent is None, "can't clone bound block" - # TODO: this might be more efficient (but trickier) with copy.copy - cloned = type(self)(*self._initializer_args[0], **self._initializer_args[1]) - cloned._cloned_from(self) - return cloned - - def _cloned_from(self: SelfType, other: SelfType) -> None: - """Copies user-specified initializers from other.""" +@non_library +class BaseContainerPort(BasePort): # TODO can this be removed? pass - def _bind(self: SelfType, parent: PortParentTypes) -> SelfType: - """Returns a clone of this object with the specified binding. This object must be unbound.""" - assert builder.get_enclosing_block() is self._block_context, f"can't clone to different context, {builder.get_enclosing_block()} -> {self._block_context}" - clone = self._clone() - clone._bind_in_place(parent) - return clone - - def _is_bound(self) -> bool: - def impl(elt: Optional[PortParentTypes]) -> bool: - if elt is None: - return False - elif isinstance(elt, BasePort): - return impl(elt._parent) - else: - return True - return impl(self._parent) - - @abstractmethod - def _get_initializers(self, path_prefix: List[str]) -> \ - List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: - """Returns all the initializers of contained parameters, as tuples of (parameter, path, initializer value). - Parameters without initializers are skipped.""" - raise NotImplementedError - -@non_library -class BaseContainerPort(BasePort): # TODO can this be removed? - pass +PortLinkType = TypeVar("PortLinkType", bound="Link", covariant=True, default="Link") # TODO: this breaks w/ selftypes -PortLinkType = TypeVar('PortLinkType', bound='Link', covariant=True, default='Link') # TODO: this breaks w/ selftypes @non_library class Port(BasePort, Generic[PortLinkType]): - """Abstract Base Class for ports""" - - SelfType = TypeVar('SelfType', bound='Port') - - link_type: Type[PortLinkType] - bridge_type: Optional[Type[PortBridge]] = None - - @classmethod - def empty(cls: Type[SelfType]) -> SelfType: - """Automatically generated empty constructor, that creates a port with all parameters None.""" - # This is kind of a really nasty hack that overwrites initializers :s - new_model = cls() - new_model._clear_initializers() - return new_model - - def __init__(self) -> None: - """Constructor for ports, structural information (parameters, fields) should be defined here - with optional initialization (for parameter defaults). - All arguments must be optional with sane (empty) defaults (for cloneability). - TODO: is this a reasonable restriction?""" - super().__init__() - - # This needs to be lazy-initialized to avoid building ports with links with ports, and so on - # TODO: maybe a cleaner solution is to mark port constructors in a Block context or Link context? - self._link_instance: Optional[PortLinkType] = None - self._bridge_instance: Optional[PortBridge] = None # internal only - self._adapter_count: int = 0 - - # TODO delete type ignore after https://github.com/python/mypy/issues/5374 - self._parameters: SubElementDict[ConstraintExpr] = self.manager.new_dict(ConstraintExpr) - - self.manager_ignored.update(['_is_connected', '_name']) - self._is_connected = BoolExpr()._bind(IsConnectedBinding(self)) - self._name = StringExpr()._bind(NameBinding(self)) - - def _clear_initializers(self) -> None: - self._parameters.finalize() - for (name, param) in self._parameters.items(): - param.initializer = None - - @override - def _cloned_from(self: SelfType, other: SelfType) -> None: - super()._cloned_from(other) - self._parameters.finalize() - for (name, param) in self._parameters.items(): - other_param = other._parameters[name] - assert isinstance(other_param, type(param)) - param.initializer = other_param.initializer - - def init_from(self: SelfType, other: SelfType) -> None: - assert self._parent is not None, "may only init_from on an bound port" - assert not self._get_initializers([]), "may only init_from an empty model" - self._cloned_from(other) - - def _bridge(self) -> Optional[PortBridge]: - """Creates a (unbound) bridge and returns it.""" - from .HierarchyBlock import Block - - if self.bridge_type is None: - return None - if self._bridge_instance is not None: - return self._bridge_instance - assert self._is_bound(), "not bound, can't create bridge" - - Block._next_bind = self.bridge_type - self._bridge_instance = self.bridge_type() - return self._bridge_instance - - ConvertTargetType = TypeVar('ConvertTargetType', bound='Port') - def _convert(self, adapter: PortAdapter[ConvertTargetType]) -> ConvertTargetType: - """Given an Adapter block, """ - from .HierarchyBlock import Block - - block_parent = self._block_parent() - if block_parent is None: - raise UnconnectableError(f"{self} must be bound to instantiate an adapter") - - enclosing_block = builder.get_enclosing_block() - assert isinstance(enclosing_block, Block) - if (block_parent is not enclosing_block) and (block_parent._parent is not enclosing_block): - raise UnconnectableError(f"can only create adapters on own ports or subblock ports") - - adapter_inst = enclosing_block.Block(adapter) - adapter_name_suffix = f"_{self._adapter_count}" if self._adapter_count > 0 else "" - enclosing_block.manager.add_element( - f"(adapter){self._name_from(enclosing_block)}{adapter_name_suffix}", - adapter_inst) - enclosing_block.connect(self, adapter_inst.src) # we don't name it to avoid explicit name conflicts - self._adapter_count += 1 - return adapter_inst.dst - - @override - def _populate_portlike_proto(self, pb: edgir.PortLike) -> None: - pb.lib_elem.target.name = self._get_def_name() - - @override - def _def_to_proto(self) -> edgir.PortTypes: - self._parameters.finalize() - - pb = edgir.Port() - - pb.self_class.target.name = self._get_def_name() - - direct_bases, indirect_bases = self._get_bases_of(Port) - for cls in direct_bases: - pb.superclasses.add().target.name = cls._static_def_name() - for cls in indirect_bases: - pb.super_superclasses.add().target.name = cls._static_def_name() - - for (name, param) in self._parameters.items(): - param._populate_decl_proto(edgir.add_pair(pb.params, name)) - - self._populate_metadata(pb.meta, self._metadata, IdentityDict()) # TODO use ref map - - return pb - - @override - def _type_of(self) -> Hashable: - return type(self) - - @override - def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: - super()._build_ref_map(ref_map, prefix) - ref_map[self.is_connected()] = edgir.localpath_concat(prefix, edgir.IS_CONNECTED) - ref_map[self.name()] = edgir.localpath_concat(prefix, edgir.NAME) - for name, param in self._parameters.items(): - param._build_ref_map(ref_map, edgir.localpath_concat(prefix, name)) - if self._link_instance is not None: - self._link_instance._build_ref_map(ref_map, edgir.localpath_concat(prefix, edgir.CONNECTED_LINK)) - - @override - def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: - self._parameters.finalize() - return [(param, path_prefix + [name], param.initializer) for (name, param) in self._parameters.items() - if param.initializer is not None] - - def is_connected(self) -> BoolExpr: - return self._is_connected - - def name(self) -> StringExpr: - return self._name - - def link(self) -> PortLinkType: - """Returns the link connected to this port, if this port is bound.""" - # TODO: with some magic, this can be implemented w/o the function call by hiding logic in getattr - if self._link_instance is not None: - return self._link_instance - assert self._is_bound(), "not bound, can't create link" - - self._link_instance = self.link_type() - self._link_instance._bind_in_place(self) - return self._link_instance - - U = TypeVar('U', bound=ConstraintExpr) - def Parameter(self, tpe: U) -> U: - """Registers a parameter for this Port""" - elt = tpe._bind(ParamBinding(self)) - self._parameters.register(elt) - return elt + """Abstract Base Class for ports""" + + SelfType = TypeVar("SelfType", bound="Port") + + link_type: Type[PortLinkType] + bridge_type: Optional[Type[PortBridge]] = None + + @classmethod + def empty(cls: Type[SelfType]) -> SelfType: + """Automatically generated empty constructor, that creates a port with all parameters None.""" + # This is kind of a really nasty hack that overwrites initializers :s + new_model = cls() + new_model._clear_initializers() + return new_model + + def __init__(self) -> None: + """Constructor for ports, structural information (parameters, fields) should be defined here + with optional initialization (for parameter defaults). + All arguments must be optional with sane (empty) defaults (for cloneability). + TODO: is this a reasonable restriction?""" + super().__init__() + + # This needs to be lazy-initialized to avoid building ports with links with ports, and so on + # TODO: maybe a cleaner solution is to mark port constructors in a Block context or Link context? + self._link_instance: Optional[PortLinkType] = None + self._bridge_instance: Optional[PortBridge] = None # internal only + self._adapter_count: int = 0 + + # TODO delete type ignore after https://github.com/python/mypy/issues/5374 + self._parameters: SubElementDict[ConstraintExpr] = self.manager.new_dict(ConstraintExpr) + + self.manager_ignored.update(["_is_connected", "_name"]) + self._is_connected = BoolExpr()._bind(IsConnectedBinding(self)) + self._name = StringExpr()._bind(NameBinding(self)) + + def _clear_initializers(self) -> None: + self._parameters.finalize() + for name, param in self._parameters.items(): + param.initializer = None + + @override + def _cloned_from(self: SelfType, other: SelfType) -> None: + super()._cloned_from(other) + self._parameters.finalize() + for name, param in self._parameters.items(): + other_param = other._parameters[name] + assert isinstance(other_param, type(param)) + param.initializer = other_param.initializer + + def init_from(self: SelfType, other: SelfType) -> None: + assert self._parent is not None, "may only init_from on an bound port" + assert not self._get_initializers([]), "may only init_from an empty model" + self._cloned_from(other) + + def _bridge(self) -> Optional[PortBridge]: + """Creates a (unbound) bridge and returns it.""" + from .HierarchyBlock import Block + + if self.bridge_type is None: + return None + if self._bridge_instance is not None: + return self._bridge_instance + assert self._is_bound(), "not bound, can't create bridge" + + Block._next_bind = self.bridge_type + self._bridge_instance = self.bridge_type() + return self._bridge_instance + + ConvertTargetType = TypeVar("ConvertTargetType", bound="Port") + + def _convert(self, adapter: PortAdapter[ConvertTargetType]) -> ConvertTargetType: + """Given an Adapter block,""" + from .HierarchyBlock import Block + + block_parent = self._block_parent() + if block_parent is None: + raise UnconnectableError(f"{self} must be bound to instantiate an adapter") + + enclosing_block = builder.get_enclosing_block() + assert isinstance(enclosing_block, Block) + if (block_parent is not enclosing_block) and (block_parent._parent is not enclosing_block): + raise UnconnectableError(f"can only create adapters on own ports or subblock ports") + + adapter_inst = enclosing_block.Block(adapter) + adapter_name_suffix = f"_{self._adapter_count}" if self._adapter_count > 0 else "" + enclosing_block.manager.add_element( + f"(adapter){self._name_from(enclosing_block)}{adapter_name_suffix}", adapter_inst + ) + enclosing_block.connect(self, adapter_inst.src) # we don't name it to avoid explicit name conflicts + self._adapter_count += 1 + return adapter_inst.dst + + @override + def _populate_portlike_proto(self, pb: edgir.PortLike) -> None: + pb.lib_elem.target.name = self._get_def_name() + + @override + def _def_to_proto(self) -> edgir.PortTypes: + self._parameters.finalize() + + pb = edgir.Port() + + pb.self_class.target.name = self._get_def_name() + + direct_bases, indirect_bases = self._get_bases_of(Port) + for cls in direct_bases: + pb.superclasses.add().target.name = cls._static_def_name() + for cls in indirect_bases: + pb.super_superclasses.add().target.name = cls._static_def_name() + + for name, param in self._parameters.items(): + param._populate_decl_proto(edgir.add_pair(pb.params, name)) + + self._populate_metadata(pb.meta, self._metadata, IdentityDict()) # TODO use ref map + + return pb + + @override + def _type_of(self) -> Hashable: + return type(self) + + @override + def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: + super()._build_ref_map(ref_map, prefix) + ref_map[self.is_connected()] = edgir.localpath_concat(prefix, edgir.IS_CONNECTED) + ref_map[self.name()] = edgir.localpath_concat(prefix, edgir.NAME) + for name, param in self._parameters.items(): + param._build_ref_map(ref_map, edgir.localpath_concat(prefix, name)) + if self._link_instance is not None: + self._link_instance._build_ref_map(ref_map, edgir.localpath_concat(prefix, edgir.CONNECTED_LINK)) + + @override + def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: + self._parameters.finalize() + return [ + (param, path_prefix + [name], param.initializer) + for (name, param) in self._parameters.items() + if param.initializer is not None + ] + + def is_connected(self) -> BoolExpr: + return self._is_connected + + def name(self) -> StringExpr: + return self._name + + def link(self) -> PortLinkType: + """Returns the link connected to this port, if this port is bound.""" + # TODO: with some magic, this can be implemented w/o the function call by hiding logic in getattr + if self._link_instance is not None: + return self._link_instance + assert self._is_bound(), "not bound, can't create link" + + self._link_instance = self.link_type() + self._link_instance._bind_in_place(self) + return self._link_instance + + U = TypeVar("U", bound=ConstraintExpr) + + def Parameter(self, tpe: U) -> U: + """Registers a parameter for this Port""" + elt = tpe._bind(ParamBinding(self)) + self._parameters.register(elt) + return elt @non_library class Bundle(Port[PortLinkType], BaseContainerPort, Generic[PortLinkType]): - SelfType = TypeVar('SelfType', bound='Bundle') - - def __init__(self) -> None: - super().__init__() - - self._ports: SubElementDict[Port] = self.manager.new_dict(Port) - - @override - def _clear_initializers(self) -> None: - super()._clear_initializers() - self._ports.finalize() - for (name, port) in self._ports.items(): - port._clear_initializers() - - @override - def _cloned_from(self: SelfType, other: SelfType) -> None: - super()._cloned_from(other) - for (name, port) in self._ports.items(): - other_port = other._ports[name] - assert isinstance(other_port, type(port)) - port._cloned_from(other_port) - - def with_elt_initializers(self: SelfType, replace_elts: dict[str, Port]) -> SelfType: - """Clones model-typed self, except adding initializers to elements from the input dict. - Those elements must be empty.""" - assert self._parent is None, "self must not be bound" - cloned = self._clone() - for (name, replace_port) in replace_elts.items(): - assert replace_port._parent is None, "replace_elts must not be bound" - cloned_port = cloned._ports[name] - assert isinstance(replace_port, type(cloned_port)) - assert not cloned_port._get_initializers([]), f"replace_elts sub-port {name} was not empty" - cloned_port._cloned_from(replace_port) - return cloned - - @override - def _def_to_proto(self) -> edgir.Bundle: - self._parameters.finalize() - self._ports.finalize() - - pb = edgir.Bundle() - - pb.self_class.target.name = self._get_def_name() - - direct_bases, indirect_bases = self._get_bases_of(Bundle) - for cls in direct_bases: - pb.superclasses.add().target.name = cls._static_def_name() - for cls in indirect_bases: - pb.super_superclasses.add().target.name = cls._static_def_name() - - for (name, param) in self._parameters.items(): - param._populate_decl_proto(edgir.add_pair(pb.params, name)) - for (name, port) in self._ports.items(): - port._populate_portlike_proto(edgir.add_pair(pb.ports, name)) - - self._populate_metadata(pb.meta, self._metadata, IdentityDict()) # TODO use ref map - - return pb - - @override - def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: - super()._build_ref_map(ref_map, prefix) - for name, field in self._ports.items(): - field._build_ref_map(ref_map, edgir.localpath_concat(prefix, name)) - - @override - def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: - self_initializers = super()._get_initializers(path_prefix) - self._ports.finalize() - return list(itertools.chain( - self_initializers, - *[port._get_initializers(path_prefix + [name]) for (name, port) in self._ports.items()] - )) - - T = TypeVar('T', bound=Port) - def Port(self, tpe: T, *, desc: Optional[str] = None) -> T: - """Registers a field for this Bundle""" - if not isinstance(tpe, Port): - raise EdgTypeError(f"param to Field(...)", tpe, Port) - - elt = tpe._bind(self) - self._ports.register(elt) - - return elt - + SelfType = TypeVar("SelfType", bound="Bundle") + + def __init__(self) -> None: + super().__init__() + + self._ports: SubElementDict[Port] = self.manager.new_dict(Port) + + @override + def _clear_initializers(self) -> None: + super()._clear_initializers() + self._ports.finalize() + for name, port in self._ports.items(): + port._clear_initializers() + + @override + def _cloned_from(self: SelfType, other: SelfType) -> None: + super()._cloned_from(other) + for name, port in self._ports.items(): + other_port = other._ports[name] + assert isinstance(other_port, type(port)) + port._cloned_from(other_port) + + def with_elt_initializers(self: SelfType, replace_elts: dict[str, Port]) -> SelfType: + """Clones model-typed self, except adding initializers to elements from the input dict. + Those elements must be empty.""" + assert self._parent is None, "self must not be bound" + cloned = self._clone() + for name, replace_port in replace_elts.items(): + assert replace_port._parent is None, "replace_elts must not be bound" + cloned_port = cloned._ports[name] + assert isinstance(replace_port, type(cloned_port)) + assert not cloned_port._get_initializers([]), f"replace_elts sub-port {name} was not empty" + cloned_port._cloned_from(replace_port) + return cloned + + @override + def _def_to_proto(self) -> edgir.Bundle: + self._parameters.finalize() + self._ports.finalize() + + pb = edgir.Bundle() + + pb.self_class.target.name = self._get_def_name() + + direct_bases, indirect_bases = self._get_bases_of(Bundle) + for cls in direct_bases: + pb.superclasses.add().target.name = cls._static_def_name() + for cls in indirect_bases: + pb.super_superclasses.add().target.name = cls._static_def_name() + + for name, param in self._parameters.items(): + param._populate_decl_proto(edgir.add_pair(pb.params, name)) + for name, port in self._ports.items(): + port._populate_portlike_proto(edgir.add_pair(pb.ports, name)) + + self._populate_metadata(pb.meta, self._metadata, IdentityDict()) # TODO use ref map + + return pb + + @override + def _build_ref_map(self, ref_map: Refable.RefMapType, prefix: edgir.LocalPath) -> None: + super()._build_ref_map(ref_map, prefix) + for name, field in self._ports.items(): + field._build_ref_map(ref_map, edgir.localpath_concat(prefix, name)) + + @override + def _get_initializers(self, path_prefix: List[str]) -> List[Tuple[ConstraintExpr, List[str], ConstraintExpr]]: + self_initializers = super()._get_initializers(path_prefix) + self._ports.finalize() + return list( + itertools.chain( + self_initializers, + *[port._get_initializers(path_prefix + [name]) for (name, port) in self._ports.items()], + ) + ) + + T = TypeVar("T", bound=Port) + + def Port(self, tpe: T, *, desc: Optional[str] = None) -> T: + """Registers a field for this Bundle""" + if not isinstance(tpe, Port): + raise EdgTypeError(f"param to Field(...)", tpe, Port) + + elt = tpe._bind(self) + self._ports.register(elt) + + return elt diff --git a/edg/core/Range.py b/edg/core/Range.py index d1251c369..f4ee301b4 100644 --- a/edg/core/Range.py +++ b/edg/core/Range.py @@ -5,266 +5,272 @@ class Range: - """A range type that indicates a range of values and provides utility functions. Immutable. - Ends are treated as inclusive (closed). - """ - @staticmethod - @deprecated("Use shrink_multiply") - def cancel_multiply(input_side: 'Range', output_side: 'Range') -> 'Range': - """IMPORTANT - this has REVERSED arguments of shrink_multiply!""" - return output_side.shrink_multiply(input_side) - - def shrink_multiply(self, contributing: 'Range') -> 'Range': + """A range type that indicates a range of values and provides utility functions. Immutable. + Ends are treated as inclusive (closed). """ - A tolerance-shrinking multiply operation, used in calculating how much tolerance remains - in a multiply expression, to reach a target tolerance given some contributing tolerance. - THIS MAY FAIL if the contributing tolerance is larger than the self (target) tolerance - (so no result would satisfy the original equation). - EXAMPLE: given the RC low pass filter equation R * C = 1 / (2 * pi * w), - we want to find C (including available tolerance) given a target w (with specified tolerance), - and some R (which eats into the tolerance budget from w) - - C = 1/(2 * pi * w).shrink_multiply(1/R) - - Prefer RangeExpr.shrink_multiply, unless you need to do additional operations on concrete values. - - - More detailed theory: - - This satisfies the property, for Range x: - x.shrink_multiply(1/x) = Range(1, 1) - - Range multiplication is weird and 1/x * x does not cancel out, because it's tolerance-expanding. - Using the RC frequency example, if we want instead solve for C given R and target w, - we can't simply do C = 1/(2 * pi * R * w) - which would be tolerance-expanding from R and w to C. - - To understand why, it helps to break ranges into tuples: - - [w_max = 1/2pi * [1/R_min * [1/C_min - w_min] 1/R_max] 1/C_max] - - Note that to cancel the tuples (so they equal [1, 1], we need to invert the components - without flipping the order. For example, to move the C to the left side, we need to multiply - both sides by: - - [C_min - C_max] - - which itself is an invalid range, since the top element is lower than the bottom element. - Doing the same with w, this resolves into - - [C_min = 1/2pi * [1/R_min * [1/w_max - C_max] 1/R_max] 1/w_min] - - So that this function does is: - flip(contributing * flip(self)) - """ - assert isinstance(contributing, Range) - lower = contributing.upper * self.lower - upper = contributing.lower * self.upper - assert lower <= upper, f"empty range in shrink-multiply {contributing} and {self}" - return Range(lower, upper) - - @staticmethod - def from_tolerance(center: float, tolerance: Union[float, Tuple[float, float]]) -> 'Range': - """Creates a Range given a center value and normalized tolerance percentage. - If a single tolerance is given, it is treated as bidirectional (+/- value). - If a tuple of two values is given, it is treated as negative, then positive tolerance.""" - if isinstance(tolerance, tuple): - assert tolerance[0] <= tolerance[1], f"invalid tolerance {tolerance}" - return Range(center * (1 + tolerance[0]), center * (1 + tolerance[1])) - elif isinstance(tolerance, (float, int)): - assert tolerance >= 0, f"bidirectional tolerance {tolerance} must be positive" - return Range(center * (1 - tolerance), center * (1 + tolerance)) - else: - raise ValueError(f"unknown tolerance format {tolerance}") - - @staticmethod - def from_abs_tolerance(center: float, tolerance: Union[float, Tuple[float, float]]) -> 'Range': - """Creates a Range given a center value and absolute tolerance. - If a single tolerance is given, it is treated as bidirectional (+/- value). - If a tuple of two values is given, it is treated as negative, then positive tolerance.""" - if isinstance(tolerance, tuple): - assert tolerance[0] <= tolerance[1], f"invalid tolerance {tolerance}" - return Range(center + tolerance[0], center + tolerance[1]) - elif isinstance(tolerance, (float, int)): - assert tolerance >= 0, f"bidirectional tolerance {tolerance} must be positive" - return Range(center - tolerance, center + tolerance) - else: - raise ValueError(f"unknown tolerance format {tolerance}") - - @staticmethod - def from_lower(lower: float) -> 'Range': - """Creates a Range from lower to positive infinity""" - return Range(lower, float('inf')) - - @staticmethod - def from_upper(upper: float) -> 'Range': - """Creates a Range from negative infinity to upper""" - return Range(float('-inf'), upper) - - @staticmethod - def zero_to_upper(upper: float) -> 'Range': - """Creates a Range from zero to upper""" - return Range(0, upper) - - @staticmethod - def exact(value: float) -> 'Range': - """Creates a Range that is exactly this value (no tolerance)""" - return Range(value, value) - - @staticmethod - def all() -> 'Range': - """Creates a Range that is a superset of every range""" - return Range(float('-inf'), float('inf')) - - @override - def __repr__(self) -> str: - return f"Range({self.lower, self.upper})" - - def __init__(self, lower: float, upper: float) -> None: - assert lower <= upper or (math.isnan(lower) and math.isnan(upper)), \ - f"invalid range with lower {lower} > upper {upper}" - self.lower = float(lower) - self.upper = float(upper) - - @override - def __eq__(self, other: Any) -> bool: - if not isinstance(other, Range): - return False - return self.lower == other.lower and self.upper == other.upper - - def center(self) -> float: - return (self.lower + self.upper) / 2 - - def __contains__(self, item: Union['Range', float]) -> bool: - """Return whether other range or float is contained (a subset of) this range.""" - if isinstance(item, (float, int)): - return self.lower <= item <= self.upper - elif isinstance(item, Range): - return self.lower <= item.lower and item.upper <= self.upper - else: - return NotImplemented - - def hull(self, other: 'Range') -> 'Range': - return Range(min(self.lower, other.lower), max(self.upper, other.upper)) - - def intersects(self, other: 'Range') -> bool: - return (self.upper >= other.lower) and (self.lower <= other.upper) - - def intersect(self, other: 'Range') -> 'Range': - # TODO make behavior more consistent w/ compiler and returning empty that props as a unit - if not self.intersects(other): - raise ValueError("cannot intersect ranges that do not intersect") - return Range(max(self.lower, other.lower), min(self.upper, other.upper)) - - def __add__(self, other: Union['Range', float]) -> 'Range': - if isinstance(other, Range): - return Range(self.lower + other.lower, self.upper + other.upper) - elif isinstance(other, (float, int)): - return Range(self.lower + other, self.upper + other) - else: - return NotImplemented - - def __sub__(self, other: float) -> 'Range': - # Range-range subtract semantics not defined as tolerance handling is not intuitive - if isinstance(other, (float, int)): - return Range(self.lower - other, self.upper - other) - else: - return NotImplemented - - def __rsub__(self, other: float) -> 'Range': - # Range-range subtract semantics not defined as tolerance handling is not intuitive - if isinstance(other, (float, int)): - return Range(other - self.upper, other - self.lower) - else: - return NotImplemented - - def __mul__(self, other: Union['Range', float]) -> 'Range': - if isinstance(other, Range): - corners = [self.lower * other.lower, - self.lower * other.upper, - self.upper * other.lower, - self.upper * other.upper] - return Range(min(corners), max(corners)) - elif isinstance(other, (float, int)): - if other >= 0: - return Range(self.lower * other, self.upper * other) - else: - return Range(self.upper * other, self.lower * other) - else: - return NotImplemented - - def __rmul__(self, other: float) -> 'Range': - if isinstance(other, (float, int)): - if other >= 0: - return Range(other * self.lower, other * self.upper) - else: - return Range(other * self.upper, other * self.lower) - else: - return NotImplemented - - def __truediv__(self, other: Union['Range', float]) -> 'Range': - if isinstance(other, Range): - assert other.lower >= 0 or other.upper <= 0, "TODO invert with range crossing zero not supported" - corners = [self.lower / other.lower, - self.lower / other.upper, - self.upper / other.lower, - self.upper / other.upper] - return Range(min(corners), max(corners)) - elif isinstance(other, (float, int)): - if other >= 0: - return Range(self.lower / other, self.upper / other) - else: - return Range(self.upper / other, self.lower / other) - else: - return NotImplemented - - def __rtruediv__(self, other: float) -> 'Range': - assert self.lower >= 0 or self.upper <= 0, "TODO invert with range crossing zero not supported" - if isinstance(other, (float, int)): - return Range(other / self.upper, other / self.lower) - else: - return NotImplemented - - def extend_upper_to(self, new_upper: float) -> 'Range': - if new_upper > self.upper: - return Range(self.lower, new_upper) - else: - return self - - def bound_to(self, bounds: 'Range') -> 'Range': - """Adjusts this range to be within the input bounds. - If the ranges intersect, returns the intersection. - If the ranges do not intersect, returns the point at the closer edge of the bounds.""" - if self.lower < bounds.lower: - new_lower = bounds.lower - elif self.lower > bounds.upper: - new_lower = bounds.upper - else: - new_lower = self.lower - - if self.upper < bounds.lower: - new_upper = bounds.lower - elif self.upper > bounds.upper: - new_upper = bounds.upper - else: - new_upper = self.upper - - return Range(new_lower, new_upper) - - DOUBLE_FLOAT_ROUND_FACTOR = 1e-7 # approximate multiplier to account for double <-> float rounding issues - - def fuzzy_in(self, container: 'Range') -> bool: - """Contains operation that allows fuzziness due to floating point imprecision.""" - if container.lower >= 0: - lower = container.lower * (1 - self.DOUBLE_FLOAT_ROUND_FACTOR) - else: - lower = container.lower * (1 + self.DOUBLE_FLOAT_ROUND_FACTOR) - - if container.upper >= 0: - upper = container.upper * (1 + self.DOUBLE_FLOAT_ROUND_FACTOR) - else: - upper = container.upper * (1 - self.DOUBLE_FLOAT_ROUND_FACTOR) - return self in Range(lower, upper) + @staticmethod + @deprecated("Use shrink_multiply") + def cancel_multiply(input_side: "Range", output_side: "Range") -> "Range": + """IMPORTANT - this has REVERSED arguments of shrink_multiply!""" + return output_side.shrink_multiply(input_side) + + def shrink_multiply(self, contributing: "Range") -> "Range": + """ + A tolerance-shrinking multiply operation, used in calculating how much tolerance remains + in a multiply expression, to reach a target tolerance given some contributing tolerance. + THIS MAY FAIL if the contributing tolerance is larger than the self (target) tolerance + (so no result would satisfy the original equation). + + EXAMPLE: given the RC low pass filter equation R * C = 1 / (2 * pi * w), + we want to find C (including available tolerance) given a target w (with specified tolerance), + and some R (which eats into the tolerance budget from w) + + C = 1/(2 * pi * w).shrink_multiply(1/R) + + Prefer RangeExpr.shrink_multiply, unless you need to do additional operations on concrete values. + + + More detailed theory: + + This satisfies the property, for Range x: + x.shrink_multiply(1/x) = Range(1, 1) + + Range multiplication is weird and 1/x * x does not cancel out, because it's tolerance-expanding. + Using the RC frequency example, if we want instead solve for C given R and target w, + we can't simply do C = 1/(2 * pi * R * w) - which would be tolerance-expanding from R and w to C. + + To understand why, it helps to break ranges into tuples: + + [w_max = 1/2pi * [1/R_min * [1/C_min + w_min] 1/R_max] 1/C_max] + + Note that to cancel the tuples (so they equal [1, 1], we need to invert the components + without flipping the order. For example, to move the C to the left side, we need to multiply + both sides by: + + [C_min + C_max] + + which itself is an invalid range, since the top element is lower than the bottom element. + Doing the same with w, this resolves into + + [C_min = 1/2pi * [1/R_min * [1/w_max + C_max] 1/R_max] 1/w_min] + + So that this function does is: + flip(contributing * flip(self)) + """ + assert isinstance(contributing, Range) + lower = contributing.upper * self.lower + upper = contributing.lower * self.upper + assert lower <= upper, f"empty range in shrink-multiply {contributing} and {self}" + return Range(lower, upper) + + @staticmethod + def from_tolerance(center: float, tolerance: Union[float, Tuple[float, float]]) -> "Range": + """Creates a Range given a center value and normalized tolerance percentage. + If a single tolerance is given, it is treated as bidirectional (+/- value). + If a tuple of two values is given, it is treated as negative, then positive tolerance.""" + if isinstance(tolerance, tuple): + assert tolerance[0] <= tolerance[1], f"invalid tolerance {tolerance}" + return Range(center * (1 + tolerance[0]), center * (1 + tolerance[1])) + elif isinstance(tolerance, (float, int)): + assert tolerance >= 0, f"bidirectional tolerance {tolerance} must be positive" + return Range(center * (1 - tolerance), center * (1 + tolerance)) + else: + raise ValueError(f"unknown tolerance format {tolerance}") + + @staticmethod + def from_abs_tolerance(center: float, tolerance: Union[float, Tuple[float, float]]) -> "Range": + """Creates a Range given a center value and absolute tolerance. + If a single tolerance is given, it is treated as bidirectional (+/- value). + If a tuple of two values is given, it is treated as negative, then positive tolerance.""" + if isinstance(tolerance, tuple): + assert tolerance[0] <= tolerance[1], f"invalid tolerance {tolerance}" + return Range(center + tolerance[0], center + tolerance[1]) + elif isinstance(tolerance, (float, int)): + assert tolerance >= 0, f"bidirectional tolerance {tolerance} must be positive" + return Range(center - tolerance, center + tolerance) + else: + raise ValueError(f"unknown tolerance format {tolerance}") + + @staticmethod + def from_lower(lower: float) -> "Range": + """Creates a Range from lower to positive infinity""" + return Range(lower, float("inf")) + + @staticmethod + def from_upper(upper: float) -> "Range": + """Creates a Range from negative infinity to upper""" + return Range(float("-inf"), upper) + + @staticmethod + def zero_to_upper(upper: float) -> "Range": + """Creates a Range from zero to upper""" + return Range(0, upper) + + @staticmethod + def exact(value: float) -> "Range": + """Creates a Range that is exactly this value (no tolerance)""" + return Range(value, value) + + @staticmethod + def all() -> "Range": + """Creates a Range that is a superset of every range""" + return Range(float("-inf"), float("inf")) + + @override + def __repr__(self) -> str: + return f"Range({self.lower, self.upper})" + + def __init__(self, lower: float, upper: float) -> None: + assert lower <= upper or ( + math.isnan(lower) and math.isnan(upper) + ), f"invalid range with lower {lower} > upper {upper}" + self.lower = float(lower) + self.upper = float(upper) + + @override + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Range): + return False + return self.lower == other.lower and self.upper == other.upper + + def center(self) -> float: + return (self.lower + self.upper) / 2 + + def __contains__(self, item: Union["Range", float]) -> bool: + """Return whether other range or float is contained (a subset of) this range.""" + if isinstance(item, (float, int)): + return self.lower <= item <= self.upper + elif isinstance(item, Range): + return self.lower <= item.lower and item.upper <= self.upper + else: + return NotImplemented + + def hull(self, other: "Range") -> "Range": + return Range(min(self.lower, other.lower), max(self.upper, other.upper)) + + def intersects(self, other: "Range") -> bool: + return (self.upper >= other.lower) and (self.lower <= other.upper) + + def intersect(self, other: "Range") -> "Range": + # TODO make behavior more consistent w/ compiler and returning empty that props as a unit + if not self.intersects(other): + raise ValueError("cannot intersect ranges that do not intersect") + return Range(max(self.lower, other.lower), min(self.upper, other.upper)) + + def __add__(self, other: Union["Range", float]) -> "Range": + if isinstance(other, Range): + return Range(self.lower + other.lower, self.upper + other.upper) + elif isinstance(other, (float, int)): + return Range(self.lower + other, self.upper + other) + else: + return NotImplemented + + def __sub__(self, other: float) -> "Range": + # Range-range subtract semantics not defined as tolerance handling is not intuitive + if isinstance(other, (float, int)): + return Range(self.lower - other, self.upper - other) + else: + return NotImplemented + + def __rsub__(self, other: float) -> "Range": + # Range-range subtract semantics not defined as tolerance handling is not intuitive + if isinstance(other, (float, int)): + return Range(other - self.upper, other - self.lower) + else: + return NotImplemented + + def __mul__(self, other: Union["Range", float]) -> "Range": + if isinstance(other, Range): + corners = [ + self.lower * other.lower, + self.lower * other.upper, + self.upper * other.lower, + self.upper * other.upper, + ] + return Range(min(corners), max(corners)) + elif isinstance(other, (float, int)): + if other >= 0: + return Range(self.lower * other, self.upper * other) + else: + return Range(self.upper * other, self.lower * other) + else: + return NotImplemented + + def __rmul__(self, other: float) -> "Range": + if isinstance(other, (float, int)): + if other >= 0: + return Range(other * self.lower, other * self.upper) + else: + return Range(other * self.upper, other * self.lower) + else: + return NotImplemented + + def __truediv__(self, other: Union["Range", float]) -> "Range": + if isinstance(other, Range): + assert other.lower >= 0 or other.upper <= 0, "TODO invert with range crossing zero not supported" + corners = [ + self.lower / other.lower, + self.lower / other.upper, + self.upper / other.lower, + self.upper / other.upper, + ] + return Range(min(corners), max(corners)) + elif isinstance(other, (float, int)): + if other >= 0: + return Range(self.lower / other, self.upper / other) + else: + return Range(self.upper / other, self.lower / other) + else: + return NotImplemented + + def __rtruediv__(self, other: float) -> "Range": + assert self.lower >= 0 or self.upper <= 0, "TODO invert with range crossing zero not supported" + if isinstance(other, (float, int)): + return Range(other / self.upper, other / self.lower) + else: + return NotImplemented + + def extend_upper_to(self, new_upper: float) -> "Range": + if new_upper > self.upper: + return Range(self.lower, new_upper) + else: + return self + + def bound_to(self, bounds: "Range") -> "Range": + """Adjusts this range to be within the input bounds. + If the ranges intersect, returns the intersection. + If the ranges do not intersect, returns the point at the closer edge of the bounds.""" + if self.lower < bounds.lower: + new_lower = bounds.lower + elif self.lower > bounds.upper: + new_lower = bounds.upper + else: + new_lower = self.lower + + if self.upper < bounds.lower: + new_upper = bounds.lower + elif self.upper > bounds.upper: + new_upper = bounds.upper + else: + new_upper = self.upper + + return Range(new_lower, new_upper) + + DOUBLE_FLOAT_ROUND_FACTOR = 1e-7 # approximate multiplier to account for double <-> float rounding issues + + def fuzzy_in(self, container: "Range") -> bool: + """Contains operation that allows fuzziness due to floating point imprecision.""" + if container.lower >= 0: + lower = container.lower * (1 - self.DOUBLE_FLOAT_ROUND_FACTOR) + else: + lower = container.lower * (1 + self.DOUBLE_FLOAT_ROUND_FACTOR) + + if container.upper >= 0: + upper = container.upper * (1 + self.DOUBLE_FLOAT_ROUND_FACTOR) + else: + upper = container.upper * (1 - self.DOUBLE_FLOAT_ROUND_FACTOR) + return self in Range(lower, upper) diff --git a/edg/core/Refinements.py b/edg/core/Refinements.py index b16a4d1cc..daa4e786f 100644 --- a/edg/core/Refinements.py +++ b/edg/core/Refinements.py @@ -13,55 +13,58 @@ # refinement value that indicates a param value (and generates into a directed assign) -class ParamValue(): - def __init__(self, path: DesignPath): - self.path = path +class ParamValue: + def __init__(self, path: DesignPath): + self.path = path -class Refinements(): - def __init__(self, class_refinements: Iterable[Tuple[Type[Block], Type[Block]]] = [], - instance_refinements: Iterable[Tuple[DesignPath, Type[Block]]] = [], - class_values: Iterable[Tuple[Type[Block], DesignPostfix, edgir.LitTypes]] = [], - instance_values: Iterable[Tuple[DesignPath, Union[edgir.LitTypes, ParamValue]]] = []): - self.class_refinements = class_refinements - self.instance_refinements = instance_refinements - self.class_values = class_values - self.instance_values = instance_values +class Refinements: + def __init__( + self, + class_refinements: Iterable[Tuple[Type[Block], Type[Block]]] = [], + instance_refinements: Iterable[Tuple[DesignPath, Type[Block]]] = [], + class_values: Iterable[Tuple[Type[Block], DesignPostfix, edgir.LitTypes]] = [], + instance_values: Iterable[Tuple[DesignPath, Union[edgir.LitTypes, ParamValue]]] = [], + ): + self.class_refinements = class_refinements + self.instance_refinements = instance_refinements + self.class_values = class_values + self.instance_values = instance_values - def populate_proto(self, pb: edgrpc.Refinements) -> edgrpc.Refinements: - # TODO: this doesn't dedup refinement. Is this desired? - # TODO explicit dedup and prioritization - # TODO some kind of override annotation? - for (src_cls, target_cls) in self.class_refinements: - ref_entry = pb.subclasses.add() - ref_entry.cls.target.name = src_cls._static_def_name() - ref_entry.replacement.target.name = target_cls._static_def_name() + def populate_proto(self, pb: edgrpc.Refinements) -> edgrpc.Refinements: + # TODO: this doesn't dedup refinement. Is this desired? + # TODO explicit dedup and prioritization + # TODO some kind of override annotation? + for src_cls, target_cls in self.class_refinements: + ref_entry = pb.subclasses.add() + ref_entry.cls.target.name = src_cls._static_def_name() + ref_entry.replacement.target.name = target_cls._static_def_name() - for (src_path, target_cls) in self.instance_refinements: - ref_entry = pb.subclasses.add() - ref_entry.path.CopyFrom(edgir.LocalPathList(src_path)) - ref_entry.replacement.target.name = target_cls._static_def_name() + for src_path, target_cls in self.instance_refinements: + ref_entry = pb.subclasses.add() + ref_entry.path.CopyFrom(edgir.LocalPathList(src_path)) + ref_entry.replacement.target.name = target_cls._static_def_name() - for (src_cls, target_subpath, target_value) in self.class_values: - val_entry = pb.values.add() - val_entry.cls_param.cls.target.name = src_cls._static_def_name() - val_entry.cls_param.param_path.CopyFrom(edgir.LocalPathList(target_subpath)) - val_entry.expr.CopyFrom(edgir.lit_to_valuelit(target_value)) + for src_cls, target_subpath, target_value in self.class_values: + val_entry = pb.values.add() + val_entry.cls_param.cls.target.name = src_cls._static_def_name() + val_entry.cls_param.param_path.CopyFrom(edgir.LocalPathList(target_subpath)) + val_entry.expr.CopyFrom(edgir.lit_to_valuelit(target_value)) - for (src_path, target_value_path) in self.instance_values: - val_entry = pb.values.add() - val_entry.path.CopyFrom(edgir.LocalPathList(src_path)) - if isinstance(target_value_path, ParamValue): - val_entry.param.CopyFrom(edgir.LocalPathList(target_value_path.path)) - else: - val_entry.expr.CopyFrom(edgir.lit_to_valuelit(target_value_path)) + for src_path, target_value_path in self.instance_values: + val_entry = pb.values.add() + val_entry.path.CopyFrom(edgir.LocalPathList(src_path)) + if isinstance(target_value_path, ParamValue): + val_entry.param.CopyFrom(edgir.LocalPathList(target_value_path.path)) + else: + val_entry.expr.CopyFrom(edgir.lit_to_valuelit(target_value_path)) - return pb + return pb - def __add__(self, rhs: Refinements) -> Refinements: - return Refinements( - class_refinements=list(chain(self.class_refinements, rhs.class_refinements)), - instance_refinements=list(chain(self.instance_refinements, rhs.instance_refinements)), - class_values=list(chain(self.class_values, rhs.class_values)), - instance_values=list(chain(self.instance_values, rhs.instance_values)), - ) + def __add__(self, rhs: Refinements) -> Refinements: + return Refinements( + class_refinements=list(chain(self.class_refinements, rhs.class_refinements)), + instance_refinements=list(chain(self.instance_refinements, rhs.instance_refinements)), + class_values=list(chain(self.class_values, rhs.class_values)), + instance_values=list(chain(self.instance_values, rhs.instance_values)), + ) diff --git a/edg/core/ScalaCompilerInterface.py b/edg/core/ScalaCompilerInterface.py index c4517dde1..3580e7641 100644 --- a/edg/core/ScalaCompilerInterface.py +++ b/edg/core/ScalaCompilerInterface.py @@ -14,138 +14,133 @@ class CompilerCheckError(BaseException): - pass + pass class CompiledDesign: - @staticmethod - def from_compiler_result(result: edgrpc.CompilerResult) -> 'CompiledDesign': - values = {value.path.SerializeToString(): edgir.valuelit_to_lit(value.value) - for value in result.solvedValues} - return CompiledDesign(result.design, values, list(result.errors)) - - @staticmethod - def from_request(design: edgir.Design, values: Iterable[edgrpc.ExprValue]) -> 'CompiledDesign': - values_dict = {value.path.SerializeToString(): edgir.valuelit_to_lit(value.value) - for value in values} - return CompiledDesign(design, values_dict, []) - - def __init__(self, design: edgir.Design, values: Dict[bytes, edgir.LitTypes], errors: List[edgrpc.ErrorRecord]): - self.design = design - self.contents = design.contents # convenience accessor - self.errors = errors - self._values = values - - def errors_str(self) -> str: - err_strs = [] - for error in self.errors: - error_pathname = edgir.local_path_to_str(error.path) - if error.name: - error_pathname += ':' + error.name - err_strs.append(f"{error.kind} @ {error_pathname}: {error.details}") - return '\n'.join([f'- {err_str}' for err_str in err_strs]) - - # Reserved.V is a string because it doesn't load properly at runtime - # Serialized strings are used since proto objects are mutable and unhashable - def get_value(self, path: Union[edgir.LocalPath, Iterable[Union[str, 'edgir.Reserved.V']]]) ->\ - Optional[edgir.LitTypes]: - if isinstance(path, edgir.LocalPath): - localpath = path - else: - localpath = edgir.LocalPathList(path) - return self._values.get(localpath.SerializeToString(), None) - - def append_values(self, values: List[Tuple[edgir.LocalPath, edgir.ValueLit]]) -> None: - """Append solved values to this design, such as from a refinement pass""" - for (value_path, value_value) in values: - value_path_str = value_path.SerializeToString() - assert value_path_str not in self._values - self._values[value_path_str] = edgir.valuelit_to_lit(value_value) + @staticmethod + def from_compiler_result(result: edgrpc.CompilerResult) -> "CompiledDesign": + values = {value.path.SerializeToString(): edgir.valuelit_to_lit(value.value) for value in result.solvedValues} + return CompiledDesign(result.design, values, list(result.errors)) + + @staticmethod + def from_request(design: edgir.Design, values: Iterable[edgrpc.ExprValue]) -> "CompiledDesign": + values_dict = {value.path.SerializeToString(): edgir.valuelit_to_lit(value.value) for value in values} + return CompiledDesign(design, values_dict, []) + + def __init__(self, design: edgir.Design, values: Dict[bytes, edgir.LitTypes], errors: List[edgrpc.ErrorRecord]): + self.design = design + self.contents = design.contents # convenience accessor + self.errors = errors + self._values = values + + def errors_str(self) -> str: + err_strs = [] + for error in self.errors: + error_pathname = edgir.local_path_to_str(error.path) + if error.name: + error_pathname += ":" + error.name + err_strs.append(f"{error.kind} @ {error_pathname}: {error.details}") + return "\n".join([f"- {err_str}" for err_str in err_strs]) + + # Reserved.V is a string because it doesn't load properly at runtime + # Serialized strings are used since proto objects are mutable and unhashable + def get_value( + self, path: Union[edgir.LocalPath, Iterable[Union[str, "edgir.Reserved.V"]]] + ) -> Optional[edgir.LitTypes]: + if isinstance(path, edgir.LocalPath): + localpath = path + else: + localpath = edgir.LocalPathList(path) + return self._values.get(localpath.SerializeToString(), None) + + def append_values(self, values: List[Tuple[edgir.LocalPath, edgir.ValueLit]]) -> None: + """Append solved values to this design, such as from a refinement pass""" + for value_path, value_value in values: + value_path_str = value_path.SerializeToString() + assert value_path_str not in self._values + self._values[value_path_str] = edgir.valuelit_to_lit(value_value) class ScalaCompilerInstance: - kDevRelpath = "../../compiler/target/scala-2.13/edg-compiler-assembly-0.1-SNAPSHOT.jar" - kPrecompiledRelpath = "resources/edg-compiler-precompiled.jar" - - def __init__(self) -> None: - self.process: Optional[Any] = None - - def check_started(self) -> None: - if self.process is None: - dev_path = os.path.join(os.path.dirname(__file__), self.kDevRelpath) - precompiled_path = os.path.join(os.path.dirname(__file__), self.kPrecompiledRelpath) - if os.path.exists(dev_path): - jar_path = dev_path - print("Using development JAR") - elif os.path.exists(precompiled_path): - jar_path = precompiled_path - else: - raise ValueError(f"No EDG Compiler JAR found") - - self.process = subprocess.Popen( - ['java', '-jar', jar_path], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - - def compile(self, block: Type[Block], refinements: Refinements = Refinements(), *, - ignore_errors: bool = False) -> CompiledDesign: - from ..hdl_server.__main__ import process_request - self.check_started() - - assert self.process is not None - assert self.process.stdin is not None - assert self.process.stdout is not None - request_serializer = BufferSerializer[edgrpc.CompilerRequest](self.process.stdin) - - block_obj = block() - request = edgrpc.CompilerRequest( - design=edgir.Design( - contents=builder.elaborate_toplevel(block_obj)) - ) - if isinstance(block_obj, DesignTop): - refinements = block_obj.refinements() + refinements - refinements.populate_proto(request.refinements) - - # write the initial request to the compiler process - request_serializer.write(request) - - # until the compiler gives back the response, this acts as the HDL server, - # taking requests in the opposite direction - assert self.process.stdin is not None - assert self.process.stdout is not None - hdl_request_deserializer = BufferDeserializer(edgrpc.HdlRequest, self.process.stdout) - hdl_response_serializer = BufferSerializer[edgrpc.HdlResponse](self.process.stdin) - while True: - sys.stdout.buffer.write(hdl_request_deserializer.read_stdout()) - sys.stdout.buffer.flush() - hdl_request = hdl_request_deserializer.read() - assert hdl_request is not None - hdl_response = process_request(hdl_request) - if hdl_response is None: - break - hdl_response_serializer.write(hdl_response) - - response_deserializer = BufferDeserializer(edgrpc.CompilerResult, self.process.stdout) - result = response_deserializer.read() - - sys.stdout.buffer.write(response_deserializer.read_stdout()) - sys.stdout.buffer.flush() - - assert result is not None - assert result.HasField('design') - design = CompiledDesign.from_compiler_result(result) - if result.errors and not ignore_errors: - raise CompilerCheckError(f"error during compilation:\n{design.errors_str()}") - return design - - def close(self) -> None: - assert self.process is not None - self.process.stdin.close() - self.process.stdout.close() - self.process.stderr.close() - self.process.wait() + kDevRelpath = "../../compiler/target/scala-2.13/edg-compiler-assembly-0.1-SNAPSHOT.jar" + kPrecompiledRelpath = "resources/edg-compiler-precompiled.jar" + + def __init__(self) -> None: + self.process: Optional[Any] = None + + def check_started(self) -> None: + if self.process is None: + dev_path = os.path.join(os.path.dirname(__file__), self.kDevRelpath) + precompiled_path = os.path.join(os.path.dirname(__file__), self.kPrecompiledRelpath) + if os.path.exists(dev_path): + jar_path = dev_path + print("Using development JAR") + elif os.path.exists(precompiled_path): + jar_path = precompiled_path + else: + raise ValueError(f"No EDG Compiler JAR found") + + self.process = subprocess.Popen( + ["java", "-jar", jar_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + def compile( + self, block: Type[Block], refinements: Refinements = Refinements(), *, ignore_errors: bool = False + ) -> CompiledDesign: + from ..hdl_server.__main__ import process_request + + self.check_started() + + assert self.process is not None + assert self.process.stdin is not None + assert self.process.stdout is not None + request_serializer = BufferSerializer[edgrpc.CompilerRequest](self.process.stdin) + + block_obj = block() + request = edgrpc.CompilerRequest(design=edgir.Design(contents=builder.elaborate_toplevel(block_obj))) + if isinstance(block_obj, DesignTop): + refinements = block_obj.refinements() + refinements + refinements.populate_proto(request.refinements) + + # write the initial request to the compiler process + request_serializer.write(request) + + # until the compiler gives back the response, this acts as the HDL server, + # taking requests in the opposite direction + assert self.process.stdin is not None + assert self.process.stdout is not None + hdl_request_deserializer = BufferDeserializer(edgrpc.HdlRequest, self.process.stdout) + hdl_response_serializer = BufferSerializer[edgrpc.HdlResponse](self.process.stdin) + while True: + sys.stdout.buffer.write(hdl_request_deserializer.read_stdout()) + sys.stdout.buffer.flush() + hdl_request = hdl_request_deserializer.read() + assert hdl_request is not None + hdl_response = process_request(hdl_request) + if hdl_response is None: + break + hdl_response_serializer.write(hdl_response) + + response_deserializer = BufferDeserializer(edgrpc.CompilerResult, self.process.stdout) + result = response_deserializer.read() + + sys.stdout.buffer.write(response_deserializer.read_stdout()) + sys.stdout.buffer.flush() + + assert result is not None + assert result.HasField("design") + design = CompiledDesign.from_compiler_result(result) + if result.errors and not ignore_errors: + raise CompilerCheckError(f"error during compilation:\n{design.errors_str()}") + return design + + def close(self) -> None: + assert self.process is not None + self.process.stdin.close() + self.process.stdout.close() + self.process.stderr.close() + self.process.wait() ScalaCompiler = ScalaCompilerInstance() diff --git a/edg/core/TransformUtil.py b/edg/core/TransformUtil.py index 8b2c1bc8e..a51e76c33 100644 --- a/edg/core/TransformUtil.py +++ b/edg/core/TransformUtil.py @@ -8,296 +8,308 @@ class PathException(Exception): - pass + pass class Path(NamedTuple): # internal helper type - blocks: Tuple[str, ...] - links: Tuple[str, ...] - ports: Tuple[str, ...] - params: Tuple[str, ...] - - @override - def __hash__(self) -> int: - return hash((self.blocks, self.links, self.ports, self.params)) - - @override - def __eq__(self, other: Any) -> bool: - return isinstance(other, Path) and self.blocks == other.blocks and self.links == other.links and \ - self.ports == other.ports and self.params == other.params - - @override - def __repr__(self) -> str: - if not self.blocks and not self.links and not self.ports and not self.params: - return '(root)' - else: - return '.'.join(self.blocks + self.links + self.ports + self.params) - - @classmethod - def empty(cls) -> Path: - return Path((), (), (), ()) - - def startswith(self, prefix: Path) -> bool: - if self.blocks == prefix.blocks: # exact match, check subpaths - if self.links == prefix.links: - if self.ports == prefix.ports: - return len(self.params) >= len(prefix.params) and self.params[:len(prefix.params)] == prefix.params - elif len(self.ports) >= len(prefix.ports) and self.ports[:len(prefix.ports)] == prefix.ports: - return (not self.params) and (not prefix.params) + blocks: Tuple[str, ...] + links: Tuple[str, ...] + ports: Tuple[str, ...] + params: Tuple[str, ...] + + @override + def __hash__(self) -> int: + return hash((self.blocks, self.links, self.ports, self.params)) + + @override + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, Path) + and self.blocks == other.blocks + and self.links == other.links + and self.ports == other.ports + and self.params == other.params + ) + + @override + def __repr__(self) -> str: + if not self.blocks and not self.links and not self.ports and not self.params: + return "(root)" + else: + return ".".join(self.blocks + self.links + self.ports + self.params) + + @classmethod + def empty(cls) -> Path: + return Path((), (), (), ()) + + def startswith(self, prefix: Path) -> bool: + if self.blocks == prefix.blocks: # exact match, check subpaths + if self.links == prefix.links: + if self.ports == prefix.ports: + return len(self.params) >= len(prefix.params) and self.params[: len(prefix.params)] == prefix.params + elif len(self.ports) >= len(prefix.ports) and self.ports[: len(prefix.ports)] == prefix.ports: + return (not self.params) and (not prefix.params) + else: + return False + + elif len(self.links) >= len(prefix.links) and self.links[: len(prefix.links)] == prefix.links: + return (not self.ports) and (not prefix.ports) and (not self.params) and (not prefix.params) + else: + return False + + elif len(self.blocks) >= len(prefix.blocks) and self.blocks[: len(prefix.blocks)] == prefix.blocks: + # partial match, check subpaths don't exist + return ( + (not self.links) + and (not prefix.links) + and (not self.ports) + and (not prefix.ports) + and (not self.params) + and (not prefix.params) + ) + else: # no match + return False + + def append_block(self, *names: str) -> Path: + assert not self.links and not self.ports and not self.params, f"tried to append block {names} to {self}" + return Path(self.blocks + tuple(names), self.links, self.ports, self.params) + + def append_link(self, *names: str) -> Path: + assert not self.ports and not self.params, f"tried to append link {names} to {self}" + return Path(self.blocks, self.links + tuple(names), self.ports, self.params) + + def append_port(self, *names: str) -> Path: + assert not self.params, f"tried to append port {names} to {self}" + return Path(self.blocks, self.links, self.ports + tuple(names), self.params) + + def append_param(self, name: str) -> Path: + return Path(self.blocks, self.links, self.ports, self.params + (name,)) + + def block_component(self) -> Path: + return Path(self.blocks, (), (), ()) + + def link_component(self, must_have_link: bool = True) -> Path: + if must_have_link: + assert self.links + return Path(self.blocks, self.links, (), ()) + + def to_tuple(self) -> Tuple[str, ...]: + return self.blocks + self.links + self.ports + self.params + + def to_local_path(self) -> edgir.LocalPath: + path = edgir.LocalPath() + for block in self.blocks: + path.steps.add().name = block + for link in self.links: + path.steps.add().name = link + for port in self.ports: + path.steps.add().name = port + for param in self.params: + path.steps.add().name = param + return path + + def _follow_partial_steps( + self, steps: List[edgir.LocalStep], curr: edgir.EltTypes + ) -> Tuple[Optional[List[edgir.LocalStep]], Tuple[Path, edgir.EltTypes]]: + if not steps: + return None, (self, curr) + elif steps[0].HasField("name"): + name = steps[0].name + if isinstance(curr, (edgir.Port, edgir.Bundle, edgir.HierarchyBlock, edgir.Link)): + param_opt = edgir.pair_get_opt(curr.params, name) + if param_opt is not None: + return self.append_param(name)._follow_partial_steps(steps[1:], param_opt) + if isinstance(curr, (edgir.Bundle, edgir.Link, edgir.HierarchyBlock, edgir.LinkArray)): + port_opt = edgir.pair_get_opt(curr.ports, name) + if port_opt is not None: + return self.append_port(name)._follow_partial_steps(steps[1:], edgir.resolve_portlike(port_opt)) + if isinstance(curr, edgir.PortArray) and curr.HasField("ports"): + port_opt = edgir.pair_get_opt(curr.ports.ports, name) + if port_opt is not None: + return self.append_port(name)._follow_partial_steps(steps[1:], edgir.resolve_portlike(port_opt)) + if isinstance(curr, edgir.HierarchyBlock): + block_opt = edgir.pair_get_opt(curr.blocks, name) + if block_opt is not None: + return self.append_block(name)._follow_partial_steps(steps[1:], edgir.resolve_blocklike(block_opt)) + if isinstance(curr, (edgir.HierarchyBlock, edgir.Link, edgir.LinkArray)): + link_opt = edgir.pair_get_opt(curr.links, name) + if link_opt is not None: + return self.append_link(name)._follow_partial_steps(steps[1:], edgir.resolve_linklike(link_opt)) + + return steps, (self, curr) + elif steps[0].HasField("reserved_param"): + return steps, (self, curr) # Path does not understand special paths + else: + raise PathException(f"unknown step {steps[0]} when following path") + + def follow_partial( + self, path: edgir.LocalPath, curr: edgir.EltTypes + ) -> Tuple[Optional[List[edgir.LocalStep]], Tuple[Path, edgir.EltTypes]]: + """Follows a edgir.LocalPath from curr, returning any unused path elements. + Returns: (unused path elements postfix, (destination full path, destination object))""" + try: + return self._follow_partial_steps(list(path.steps), curr) + except PathException as e: + import sys + + raise type(e)(f"(while following {edgir.local_path_to_str(path)} from {self}) " + str(e)).with_traceback( + sys.exc_info()[2] + ) + + def follow(self, path: edgir.LocalPath, curr: edgir.EltTypes) -> Tuple[Path, edgir.EltTypes]: + """Follows a edgir.LocalPath from curr. Raises an exception if there are unused path elements. + Returns: (destination full path, destination object)""" + remaining, (end_path, end_elt) = self.follow_partial(path, curr) + if remaining is not None: + raise ValueError( + f"can't follow {edgir.local_path_to_str(path)} from {self}, have unused path elements {remaining} from {end_path}" + ) else: - return False - - elif len(self.links) >= len(prefix.links) and self.links[:len(prefix.links)] == prefix.links: - return (not self.ports) and (not prefix.ports) and (not self.params) and (not prefix.params) - else: - return False - - elif len(self.blocks) >= len(prefix.blocks) and self.blocks[:len(prefix.blocks)] == prefix.blocks: - # partial match, check subpaths don't exist - return (not self.links) and (not prefix.links) and (not self.ports) and (not prefix.ports) and \ - (not self.params) and (not prefix.params) - else: # no match - return False - - def append_block(self, *names: str) -> Path: - assert not self.links and not self.ports and not self.params, f"tried to append block {names} to {self}" - return Path(self.blocks + tuple(names), self.links, self.ports, self.params) - - def append_link(self, *names: str) -> Path: - assert not self.ports and not self.params, f"tried to append link {names} to {self}" - return Path(self.blocks, self.links + tuple(names), self.ports, self.params) - - def append_port(self, *names: str) -> Path: - assert not self.params, f"tried to append port {names} to {self}" - return Path(self.blocks, self.links, self.ports + tuple(names), self.params) - - def append_param(self, name: str) -> Path: - return Path(self.blocks, self.links, self.ports, self.params + (name, )) - - def block_component(self) -> Path: - return Path(self.blocks, (), (), ()) - - def link_component(self, must_have_link: bool=True) -> Path: - if must_have_link: - assert self.links - return Path(self.blocks, self.links, (), ()) - - def to_tuple(self) -> Tuple[str, ...]: - return self.blocks + self.links + self.ports + self.params - - def to_local_path(self) -> edgir.LocalPath: - path = edgir.LocalPath() - for block in self.blocks: - path.steps.add().name = block - for link in self.links: - path.steps.add().name = link - for port in self.ports: - path.steps.add().name = port - for param in self.params: - path.steps.add().name = param - return path - - def _follow_partial_steps(self, steps: List[edgir.LocalStep], curr: edgir.EltTypes) -> \ - Tuple[Optional[List[edgir.LocalStep]], Tuple[Path, edgir.EltTypes]]: - if not steps: - return None, (self, curr) - elif steps[0].HasField('name'): - name = steps[0].name - if isinstance(curr, (edgir.Port, edgir.Bundle, edgir.HierarchyBlock, edgir.Link)): - param_opt = edgir.pair_get_opt(curr.params, name) - if param_opt is not None: - return self.append_param(name)._follow_partial_steps(steps[1:], param_opt) - if isinstance(curr, (edgir.Bundle, edgir.Link, edgir.HierarchyBlock, edgir.LinkArray)): - port_opt = edgir.pair_get_opt(curr.ports, name) - if port_opt is not None: - return self.append_port(name)._follow_partial_steps(steps[1:], edgir.resolve_portlike(port_opt)) - if isinstance(curr, edgir.PortArray) and curr.HasField('ports'): - port_opt = edgir.pair_get_opt(curr.ports.ports, name) - if port_opt is not None: - return self.append_port(name)._follow_partial_steps(steps[1:], edgir.resolve_portlike(port_opt)) - if isinstance(curr, edgir.HierarchyBlock): - block_opt = edgir.pair_get_opt(curr.blocks, name) - if block_opt is not None: - return self.append_block(name)._follow_partial_steps(steps[1:], edgir.resolve_blocklike(block_opt)) - if isinstance(curr, (edgir.HierarchyBlock, edgir.Link, edgir.LinkArray)): - link_opt = edgir.pair_get_opt(curr.links, name) - if link_opt is not None: - return self.append_link(name)._follow_partial_steps(steps[1:], edgir.resolve_linklike(link_opt)) - - return steps, (self, curr) - elif steps[0].HasField('reserved_param'): - return steps, (self, curr) # Path does not understand special paths - else: - raise PathException(f"unknown step {steps[0]} when following path") - - def follow_partial(self, path: edgir.LocalPath, curr: edgir.EltTypes) -> \ - Tuple[Optional[List[edgir.LocalStep]], Tuple[Path, edgir.EltTypes]]: - """Follows a edgir.LocalPath from curr, returning any unused path elements. - Returns: (unused path elements postfix, (destination full path, destination object))""" - try: - return self._follow_partial_steps(list(path.steps), curr) - except PathException as e: - import sys - raise type(e)(f"(while following {edgir.local_path_to_str(path)} from {self}) " + str(e)) \ - .with_traceback(sys.exc_info()[2]) - - def follow(self, path: edgir.LocalPath, curr: edgir.EltTypes) -> Tuple[Path, edgir.EltTypes]: - """Follows a edgir.LocalPath from curr. Raises an exception if there are unused path elements. - Returns: (destination full path, destination object)""" - remaining, (end_path, end_elt) = self.follow_partial(path, curr) - if remaining is not None: - raise ValueError(f"can't follow {edgir.local_path_to_str(path)} from {self}, have unused path elements {remaining} from {end_path}") - else: - return (end_path, end_elt) + return (end_path, end_elt) class TransformContext(NamedTuple): - path: Path - design: edgir.Design + path: Path + design: edgir.Design - @override - def __repr__(self) -> str: - return f"TransformContext(path={self.path}, design=...)" + @override + def __repr__(self) -> str: + return f"TransformContext(path={self.path}, design=...)" - def append_block(self, name: str) -> TransformContext: - return TransformContext(self.path.append_block(name), self.design) + def append_block(self, name: str) -> TransformContext: + return TransformContext(self.path.append_block(name), self.design) - def append_link(self, name: str) -> TransformContext: - return TransformContext(self.path.append_link(name), self.design) + def append_link(self, name: str) -> TransformContext: + return TransformContext(self.path.append_link(name), self.design) - def append_port(self, name: str) -> TransformContext: - return TransformContext(self.path.append_port(name), self.design) + def append_port(self, name: str) -> TransformContext: + return TransformContext(self.path.append_port(name), self.design) # Preorder traversal needed for: generators (helpful), design inst, # Postorder traversal needed for: netlisting (but not really) -class Transform(): - """ - Base class that does a preorder traversal and in-place visiting (with modification) of the Design tree. - Note, the visit_* operations are in-place to keep the local element consistent with the top-level Design reference - in the Context - """ - def visit_block(self, context: TransformContext, block: edgir.HierarchyBlock) -> None: - pass - - def visit_link(self, context: TransformContext, link: edgir.Link) -> None: - pass - - def visit_linkarray(self, context: TransformContext, link: edgir.LinkArray) -> None: - pass - - # The visit_*like allows changing the "type", eg lib -> instantiated h-block - def visit_blocklike(self, context: TransformContext, block: edgir.BlockLike) -> None: - pass - - def visit_portlike(self, context: TransformContext, port: edgir.PortLike) -> None: - pass - - def visit_linklike(self, context: TransformContext, link: edgir.LinkLike) -> None: - pass - - # - # Internal Traversal Methods - # - def _traverse_portlike(self, context: TransformContext, elt: edgir.PortLike) -> None: - try: - self.visit_portlike(context, elt) - except Exception as e: - raise type(e)(f"(while visiting PortLike at {context}) " + str(e)) \ - .with_traceback(sys.exc_info()[2]) - - if elt.HasField('lib_elem'): - raise ValueError(f"unresolved lib at {context}") - elif elt.HasField('port'): - pass # nothing to recurse into - elif elt.HasField('bundle'): - for port_pair in elt.bundle.ports: - self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) - elif elt.HasField('array') and elt.array.HasField('ports'): - for port_pair in elt.array.ports.ports: - self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) - else: - raise ValueError(f"_traverse_portlike encountered unknown type {elt} at {context}") - - def _traverse_block(self, context: TransformContext, elt: edgir.HierarchyBlock) -> None: - try: - self.visit_block(context, elt) - except Exception as e: - raise type(e)(f"(while visiting Block at {context}) " + str(e)) \ - .with_traceback(sys.exc_info()[2]) - - for port_pair in elt.ports: - self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) - for link_pair in elt.links: - self._traverse_linklike(context.append_link(link_pair.name), link_pair.value) - for block_pair in elt.blocks: - self._traverse_blocklike(context.append_block(block_pair.name), block_pair.value) - - def _traverse_blocklike(self, context: TransformContext, elt: edgir.BlockLike) -> None: - try: - self.visit_blocklike(context, elt) - except Exception as e: - raise type(e)(f"(while visiting PortLike at {context}) " + str(e)) \ - .with_traceback(sys.exc_info()[2]) - - if elt.HasField('lib_elem'): - raise ValueError(f"unresolved lib at {context}") - elif elt.HasField('hierarchy'): - self._traverse_block(context, elt.hierarchy) - else: - raise ValueError(f"_traverse_blocklike encountered unknown type {elt} at {context}") - - def _traverse_linklike(self, context: TransformContext, elt: edgir.LinkLike) -> None: - try: - self.visit_linklike(context, elt) - except Exception as e: - raise type(e)(f"(while visiting LinkLike at {context}) " + str(e)) \ - .with_traceback(sys.exc_info()[2]) - - if elt.HasField('lib_elem'): - raise ValueError(f"unresolved lib at {context}") - elif elt.HasField('link'): - try: - self.visit_link(context, elt.link) - except Exception as e: - raise type(e)(f"(while visiting Link at {context}) " + str(e)) \ - .with_traceback(sys.exc_info()[2]) - - for port_pair in elt.link.ports: - self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) - - for link_pair in elt.link.links: - self._traverse_linklike(context.append_link(link_pair.name), link_pair.value) - elif elt.HasField('array'): - try: - self.visit_linkarray(context, elt.array) - except Exception as e: - raise type(e)(f"(while visiting LinkArray at {context}) " + str(e)) \ - .with_traceback(sys.exc_info()[2]) - - for port_pair in elt.array.ports: - self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) - - for link_pair in elt.array.links: - self._traverse_linklike(context.append_link(link_pair.name), link_pair.value) - else: - raise ValueError(f"_traverse_linklike encountered unknown type {elt} at {context}") - - def transform_design(self, design: edgir.Design) -> edgir.Design: - design_copy = edgir.Design() - design_copy.CopyFrom(design) # create a copy so we can mutate in-place - - # TODO: toplevel is nonuniform w/ BlockLike, so we copy transform_blocklike and traverse_blocklike - # TODO: this can't handle instantiation at the top level - root_context = TransformContext(Path.empty(), design_copy) - self.visit_block(root_context, design_copy.contents) - - # TODO dedup w/ _transform_blocklike - for port_pair in design_copy.contents.ports: - self._traverse_portlike(root_context.append_port(port_pair.name), port_pair.value) - for link_pair in design_copy.contents.links: - self._traverse_linklike(root_context.append_link(link_pair.name), link_pair.value) - for block_pair in design_copy.contents.blocks: - self._traverse_blocklike(root_context.append_block(block_pair.name), block_pair.value) - - return design_copy +class Transform: + """ + Base class that does a preorder traversal and in-place visiting (with modification) of the Design tree. + Note, the visit_* operations are in-place to keep the local element consistent with the top-level Design reference + in the Context + """ + + def visit_block(self, context: TransformContext, block: edgir.HierarchyBlock) -> None: + pass + + def visit_link(self, context: TransformContext, link: edgir.Link) -> None: + pass + + def visit_linkarray(self, context: TransformContext, link: edgir.LinkArray) -> None: + pass + + # The visit_*like allows changing the "type", eg lib -> instantiated h-block + def visit_blocklike(self, context: TransformContext, block: edgir.BlockLike) -> None: + pass + + def visit_portlike(self, context: TransformContext, port: edgir.PortLike) -> None: + pass + + def visit_linklike(self, context: TransformContext, link: edgir.LinkLike) -> None: + pass + + # + # Internal Traversal Methods + # + def _traverse_portlike(self, context: TransformContext, elt: edgir.PortLike) -> None: + try: + self.visit_portlike(context, elt) + except Exception as e: + raise type(e)(f"(while visiting PortLike at {context}) " + str(e)).with_traceback(sys.exc_info()[2]) + + if elt.HasField("lib_elem"): + raise ValueError(f"unresolved lib at {context}") + elif elt.HasField("port"): + pass # nothing to recurse into + elif elt.HasField("bundle"): + for port_pair in elt.bundle.ports: + self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) + elif elt.HasField("array") and elt.array.HasField("ports"): + for port_pair in elt.array.ports.ports: + self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) + else: + raise ValueError(f"_traverse_portlike encountered unknown type {elt} at {context}") + + def _traverse_block(self, context: TransformContext, elt: edgir.HierarchyBlock) -> None: + try: + self.visit_block(context, elt) + except Exception as e: + raise type(e)(f"(while visiting Block at {context}) " + str(e)).with_traceback(sys.exc_info()[2]) + + for port_pair in elt.ports: + self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) + for link_pair in elt.links: + self._traverse_linklike(context.append_link(link_pair.name), link_pair.value) + for block_pair in elt.blocks: + self._traverse_blocklike(context.append_block(block_pair.name), block_pair.value) + + def _traverse_blocklike(self, context: TransformContext, elt: edgir.BlockLike) -> None: + try: + self.visit_blocklike(context, elt) + except Exception as e: + raise type(e)(f"(while visiting PortLike at {context}) " + str(e)).with_traceback(sys.exc_info()[2]) + + if elt.HasField("lib_elem"): + raise ValueError(f"unresolved lib at {context}") + elif elt.HasField("hierarchy"): + self._traverse_block(context, elt.hierarchy) + else: + raise ValueError(f"_traverse_blocklike encountered unknown type {elt} at {context}") + + def _traverse_linklike(self, context: TransformContext, elt: edgir.LinkLike) -> None: + try: + self.visit_linklike(context, elt) + except Exception as e: + raise type(e)(f"(while visiting LinkLike at {context}) " + str(e)).with_traceback(sys.exc_info()[2]) + + if elt.HasField("lib_elem"): + raise ValueError(f"unresolved lib at {context}") + elif elt.HasField("link"): + try: + self.visit_link(context, elt.link) + except Exception as e: + raise type(e)(f"(while visiting Link at {context}) " + str(e)).with_traceback(sys.exc_info()[2]) + + for port_pair in elt.link.ports: + self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) + + for link_pair in elt.link.links: + self._traverse_linklike(context.append_link(link_pair.name), link_pair.value) + elif elt.HasField("array"): + try: + self.visit_linkarray(context, elt.array) + except Exception as e: + raise type(e)(f"(while visiting LinkArray at {context}) " + str(e)).with_traceback(sys.exc_info()[2]) + + for port_pair in elt.array.ports: + self._traverse_portlike(context.append_port(port_pair.name), port_pair.value) + + for link_pair in elt.array.links: + self._traverse_linklike(context.append_link(link_pair.name), link_pair.value) + else: + raise ValueError(f"_traverse_linklike encountered unknown type {elt} at {context}") + + def transform_design(self, design: edgir.Design) -> edgir.Design: + design_copy = edgir.Design() + design_copy.CopyFrom(design) # create a copy so we can mutate in-place + + # TODO: toplevel is nonuniform w/ BlockLike, so we copy transform_blocklike and traverse_blocklike + # TODO: this can't handle instantiation at the top level + root_context = TransformContext(Path.empty(), design_copy) + self.visit_block(root_context, design_copy.contents) + + # TODO dedup w/ _transform_blocklike + for port_pair in design_copy.contents.ports: + self._traverse_portlike(root_context.append_port(port_pair.name), port_pair.value) + for link_pair in design_copy.contents.links: + self._traverse_linklike(root_context.append_link(link_pair.name), link_pair.value) + for block_pair in design_copy.contents.blocks: + self._traverse_blocklike(root_context.append_block(block_pair.name), block_pair.value) + + return design_copy diff --git a/edg/core/Util.py b/edg/core/Util.py index 0bd781fd4..76f84c5c8 100644 --- a/edg/core/Util.py +++ b/edg/core/Util.py @@ -3,29 +3,31 @@ from .Blocks import BaseBlock, Link from .HierarchyBlock import Block + def edg_obj_name(obj: Any) -> str: - return "%s@%02x" % (obj.get_ref_name(), (id(obj)//4) & 0xff) + return "%s@%02x" % (obj.get_ref_name(), (id(obj) // 4) & 0xFF) + def edg_to_dict(obj: Union[BasePort, BaseBlock]) -> Dict[str, Any]: - if isinstance(obj, Bundle): - return { - '_': edg_obj_name(obj), - 'params': obj._parameters, - 'fields': {k: edg_to_dict(v) for k, v in obj._ports.items()}, - } - elif isinstance(obj, Port): - result = { - '_': edg_obj_name(obj), - 'params': obj._parameters, - } - if obj._link_instance is not None: - result['link'] = edg_to_dict(obj._link_instance) - return result - elif isinstance(obj, Block) or isinstance(obj, Link): - return { - '_': edg_obj_name(obj), - 'params': obj._parameters, - 'ports': {k: edg_to_dict(v) for k, v in obj._ports.items()}, - } - else: - raise ValueError + if isinstance(obj, Bundle): + return { + "_": edg_obj_name(obj), + "params": obj._parameters, + "fields": {k: edg_to_dict(v) for k, v in obj._ports.items()}, + } + elif isinstance(obj, Port): + result = { + "_": edg_obj_name(obj), + "params": obj._parameters, + } + if obj._link_instance is not None: + result["link"] = edg_to_dict(obj._link_instance) + return result + elif isinstance(obj, Block) or isinstance(obj, Link): + return { + "_": edg_obj_name(obj), + "params": obj._parameters, + "ports": {k: edg_to_dict(v) for k, v in obj._ports.items()}, + } + else: + raise ValueError diff --git a/edg/core/test_assertion.py b/edg/core/test_assertion.py index b3753ceda..679afce35 100644 --- a/edg/core/test_assertion.py +++ b/edg/core/test_assertion.py @@ -4,23 +4,23 @@ class TestAssertionFailureBlock(Block): - def __init__(self) -> None: - super().__init__() - self.require(BoolExpr._to_expr_type(False)) + def __init__(self) -> None: + super().__init__() + self.require(BoolExpr._to_expr_type(False)) class TestAssertionMissingBlock(Block): - def __init__(self) -> None: - super().__init__() - self.missing = self.Parameter(BoolExpr()) - self.require(self.missing) + def __init__(self) -> None: + super().__init__() + self.missing = self.Parameter(BoolExpr()) + self.require(self.missing) class AssertionTestCase(unittest.TestCase): - def test_failure(self) -> None: - with self.assertRaises(CompilerCheckError): - ScalaCompiler.compile(TestAssertionFailureBlock) + def test_failure(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(TestAssertionFailureBlock) - def test_missing(self) -> None: - with self.assertRaises(CompilerCheckError): - ScalaCompiler.compile(TestAssertionMissingBlock) + def test_missing(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(TestAssertionMissingBlock) diff --git a/edg/core/test_block.py b/edg/core/test_block.py index 362a07e08..f547e8817 100644 --- a/edg/core/test_block.py +++ b/edg/core/test_block.py @@ -9,143 +9,147 @@ @abstract_block class TestBlockBase(Block): - def __init__(self) -> None: - super().__init__() - self.base_float = self.Parameter(FloatExpr()) - self.base_port = self.Port(TestPortBase()) # required to test required constraint - self.base_port_constr = self.Port(TestPortBase(self.base_float), optional=True) + def __init__(self) -> None: + super().__init__() + self.base_float = self.Parameter(FloatExpr()) + self.base_port = self.Port(TestPortBase()) # required to test required constraint + self.base_port_constr = self.Port(TestPortBase(self.base_float), optional=True) @abstract_block_default(lambda: TestBlock) class TestBlockSecondBase(Block): - pass + pass class TestBlockSecondSub(TestBlockSecondBase): # test indirect classes - pass + pass + @non_library class TestBlockSecondNonLibrary(TestBlockSecondSub): # test that it can skip the non_library superclass - pass + pass class TestBlock(TestBlockBase, TestBlockSecondNonLibrary): - def __init__(self) -> None: - super().__init__() - self.range_init = self.Parameter(RangeExpr((-4.2, -1.3))) - self.array_init = self.Parameter(ArrayBoolExpr([False, True, False])) - self.array_empty = self.Parameter(ArrayStringExpr([])) - self.port_lit = self.Port(TestPortBase(117), optional=True) + def __init__(self) -> None: + super().__init__() + self.range_init = self.Parameter(RangeExpr((-4.2, -1.3))) + self.array_init = self.Parameter(ArrayBoolExpr([False, True, False])) + self.array_empty = self.Parameter(ArrayStringExpr([])) + self.port_lit = self.Port(TestPortBase(117), optional=True) class BlockBaseProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockBase()._elaborated_def_to_proto() - - def test_abstract(self) -> None: - self.assertEqual(self.pb.is_abstract, True) - self.assertEqual(self.pb.HasField('default_refinement'), False) - - def test_param_def(self) -> None: - self.assertEqual(len(self.pb.params), 1) - self.assertEqual(self.pb.params[0].name, 'base_float') - self.assertTrue(self.pb.params[0].value.HasField('floating')) - - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 2) - self.assertEqual(self.pb.ports[0].name, 'base_port') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") - self.assertEqual(self.pb.ports[1].name, 'base_port_constr') - self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") - - def test_connected_constraint(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.ref.steps.add().name = 'base_port' - expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED - self.assertEqual(self.pb.constraints[0].name, "(reqd)base_port") - self.assertEqual(self.pb.constraints[0].value, expected_constr) - - def test_port_init(self) -> None: - self.assertEqual(self.pb.constraints[1].name, "(init)base_port_constr.float_param") - self.assertEqual(self.pb.constraints[1].value, edgir.AssignRef(['base_port_constr', 'float_param'], ['base_float'])) + @override + def setUp(self) -> None: + self.pb = TestBlockBase()._elaborated_def_to_proto() + + def test_abstract(self) -> None: + self.assertEqual(self.pb.is_abstract, True) + self.assertEqual(self.pb.HasField("default_refinement"), False) + + def test_param_def(self) -> None: + self.assertEqual(len(self.pb.params), 1) + self.assertEqual(self.pb.params[0].name, "base_float") + self.assertTrue(self.pb.params[0].value.HasField("floating")) + + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 2) + self.assertEqual(self.pb.ports[0].name, "base_port") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") + self.assertEqual(self.pb.ports[1].name, "base_port_constr") + self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") + + def test_connected_constraint(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.ref.steps.add().name = "base_port" + expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED + self.assertEqual(self.pb.constraints[0].name, "(reqd)base_port") + self.assertEqual(self.pb.constraints[0].value, expected_constr) + + def test_port_init(self) -> None: + self.assertEqual(self.pb.constraints[1].name, "(init)base_port_constr.float_param") + self.assertEqual( + self.pb.constraints[1].value, edgir.AssignRef(["base_port_constr", "float_param"], ["base_float"]) + ) class BlockSecondBaseProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockSecondBase()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = TestBlockSecondBase()._elaborated_def_to_proto() - def test_abstract(self) -> None: - self.assertEqual(self.pb.is_abstract, True) + def test_abstract(self) -> None: + self.assertEqual(self.pb.is_abstract, True) - def test_default(self) -> None: - self.assertEqual(self.pb.default_refinement.target.name, "edg.core.test_block.TestBlock") + def test_default(self) -> None: + self.assertEqual(self.pb.default_refinement.target.name, "edg.core.test_block.TestBlock") class BlockProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlock()._elaborated_def_to_proto() - - def test_not_abstract(self) -> None: - self.assertEqual(self.pb.is_abstract, False) - self.assertEqual(self.pb.HasField('default_refinement'), False) - - def test_superclass(self) -> None: - self.assertEqual(self.pb.self_class.target.name, "edg.core.test_block.TestBlock") - self.assertEqual(self.pb.prerefine_class.target.name, "edg.core.test_block.TestBlock") - self.assertEqual(len(self.pb.superclasses), 2) - self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_block.TestBlockBase") - self.assertEqual(self.pb.superclasses[1].target.name, "edg.core.test_block.TestBlockSecondSub") - self.assertEqual(len(self.pb.super_superclasses), 1) - self.assertEqual(self.pb.super_superclasses[0].target.name, "edg.core.test_block.TestBlockSecondBase") - - self.assertEqual(self.pb.params[0].name, "base_float") - self.assertTrue(self.pb.params[0].value.HasField('floating')) - self.assertEqual(self.pb.ports[0].name, "base_port") - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") - self.assertEqual(self.pb.ports[1].name, "base_port_constr") - self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") - - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 3) - self.assertEqual(self.pb.ports[2].name, "port_lit") - self.assertEqual(self.pb.ports[2].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") - self.assertEqual(self.pb.constraints[0].name, "(reqd)base_port") - - def test_param_def(self) -> None: - self.assertEqual(len(self.pb.params), 4) - self.assertEqual(self.pb.params[1].name, "range_init") - self.assertTrue(self.pb.params[1].value.HasField('range')) - self.assertEqual(self.pb.params[2].name, "array_init") - self.assertTrue(self.pb.params[2].value.HasField('array')) - self.assertEqual(self.pb.params[3].name, "array_empty") - self.assertTrue(self.pb.params[3].value.HasField('array')) - - def test_superclass_init(self) -> None: - self.assertEqual(self.pb.constraints[1].name, "(init)base_port_constr.float_param") - self.assertEqual(self.pb.constraints[1].value, edgir.AssignRef(['base_port_constr', 'float_param'], ['base_float'])) - - def test_port_init(self) -> None: - self.assertEqual(self.pb.constraints[2].name, "(init)port_lit.float_param") - - def test_param_init(self) -> None: - self.assertEqual(self.pb.constraints[3].name, "(init)range_init") - self.assertEqual(self.pb.constraints[3].value, edgir.AssignLit(['range_init'], Range(-4.2, -1.3))) - - expected_assign = edgir.ValueExpr() - expected_assign.assign.dst.CopyFrom(edgir.LocalPathList(['array_init'])) - expected_array = expected_assign.assign.src.array - expected_array.vals.add().CopyFrom(edgir.lit_to_expr(False)) - expected_array.vals.add().CopyFrom(edgir.lit_to_expr(True)) - expected_array.vals.add().CopyFrom(edgir.lit_to_expr(False)) - self.assertEqual(self.pb.constraints[4].name, "(init)array_init") - self.assertEqual(self.pb.constraints[4].value, expected_assign) - - expected_assign = edgir.ValueExpr() - expected_assign.assign.dst.CopyFrom(edgir.LocalPathList(['array_empty'])) - expected_assign.assign.src.array.SetInParent() - self.assertEqual(self.pb.constraints[5].name, "(init)array_empty") - self.assertEqual(self.pb.constraints[5].value, expected_assign) - + @override + def setUp(self) -> None: + self.pb = TestBlock()._elaborated_def_to_proto() + + def test_not_abstract(self) -> None: + self.assertEqual(self.pb.is_abstract, False) + self.assertEqual(self.pb.HasField("default_refinement"), False) + + def test_superclass(self) -> None: + self.assertEqual(self.pb.self_class.target.name, "edg.core.test_block.TestBlock") + self.assertEqual(self.pb.prerefine_class.target.name, "edg.core.test_block.TestBlock") + self.assertEqual(len(self.pb.superclasses), 2) + self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_block.TestBlockBase") + self.assertEqual(self.pb.superclasses[1].target.name, "edg.core.test_block.TestBlockSecondSub") + self.assertEqual(len(self.pb.super_superclasses), 1) + self.assertEqual(self.pb.super_superclasses[0].target.name, "edg.core.test_block.TestBlockSecondBase") + + self.assertEqual(self.pb.params[0].name, "base_float") + self.assertTrue(self.pb.params[0].value.HasField("floating")) + self.assertEqual(self.pb.ports[0].name, "base_port") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") + self.assertEqual(self.pb.ports[1].name, "base_port_constr") + self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") + + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 3) + self.assertEqual(self.pb.ports[2].name, "port_lit") + self.assertEqual(self.pb.ports[2].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortBase") + self.assertEqual(self.pb.constraints[0].name, "(reqd)base_port") + + def test_param_def(self) -> None: + self.assertEqual(len(self.pb.params), 4) + self.assertEqual(self.pb.params[1].name, "range_init") + self.assertTrue(self.pb.params[1].value.HasField("range")) + self.assertEqual(self.pb.params[2].name, "array_init") + self.assertTrue(self.pb.params[2].value.HasField("array")) + self.assertEqual(self.pb.params[3].name, "array_empty") + self.assertTrue(self.pb.params[3].value.HasField("array")) + + def test_superclass_init(self) -> None: + self.assertEqual(self.pb.constraints[1].name, "(init)base_port_constr.float_param") + self.assertEqual( + self.pb.constraints[1].value, edgir.AssignRef(["base_port_constr", "float_param"], ["base_float"]) + ) + + def test_port_init(self) -> None: + self.assertEqual(self.pb.constraints[2].name, "(init)port_lit.float_param") + + def test_param_init(self) -> None: + self.assertEqual(self.pb.constraints[3].name, "(init)range_init") + self.assertEqual(self.pb.constraints[3].value, edgir.AssignLit(["range_init"], Range(-4.2, -1.3))) + + expected_assign = edgir.ValueExpr() + expected_assign.assign.dst.CopyFrom(edgir.LocalPathList(["array_init"])) + expected_array = expected_assign.assign.src.array + expected_array.vals.add().CopyFrom(edgir.lit_to_expr(False)) + expected_array.vals.add().CopyFrom(edgir.lit_to_expr(True)) + expected_array.vals.add().CopyFrom(edgir.lit_to_expr(False)) + self.assertEqual(self.pb.constraints[4].name, "(init)array_init") + self.assertEqual(self.pb.constraints[4].value, expected_assign) + + expected_assign = edgir.ValueExpr() + expected_assign.assign.dst.CopyFrom(edgir.LocalPathList(["array_empty"])) + expected_assign.assign.src.array.SetInParent() + self.assertEqual(self.pb.constraints[5].name, "(init)array_empty") + self.assertEqual(self.pb.constraints[5].value, expected_assign) diff --git a/edg/core/test_block_description.py b/edg/core/test_block_description.py index 60d773df9..a05d341c3 100644 --- a/edg/core/test_block_description.py +++ b/edg/core/test_block_description.py @@ -6,11 +6,13 @@ class DescriptionBlock(Block): """Block with a float parameter and description""" + def __init__(self) -> None: super().__init__() self.variable = self.Parameter(FloatExpr(5.0)) self.description = DescriptionString( - "Test variable = ", DescriptionString.FormatUnits(self.variable, "Units"), ".") + "Test variable = ", DescriptionString.FormatUnits(self.variable, "Units"), "." + ) class DescriptionBlockProtoTestCase(unittest.TestCase): @@ -19,9 +21,9 @@ def test_description(self) -> None: self.assertEqual(len(pb.description), 3) - self.assertEqual(pb.description[0].text, 'Test variable = ') + self.assertEqual(pb.description[0].text, "Test variable = ") expected_ref = edgir.LocalPath() - expected_ref.steps.add().name = 'variable' + expected_ref.steps.add().name = "variable" self.assertEqual(pb.description[1].param.path, expected_ref) - self.assertEqual(pb.description[1].param.unit, 'Units') - self.assertEqual(pb.description[2].text, '.') + self.assertEqual(pb.description[1].param.unit, "Units") + self.assertEqual(pb.description[2].text, ".") diff --git a/edg/core/test_block_eltdict.py b/edg/core/test_block_eltdict.py index 65c7b536a..db95ec6b1 100644 --- a/edg/core/test_block_eltdict.py +++ b/edg/core/test_block_eltdict.py @@ -7,28 +7,29 @@ class EltDictBlock(Block): - """Block with an EltDict of sub-blocks""" - @override - def contents(self) -> None: - super().contents() - self.sink = ElementDict[Block]() - self.sink[0] = self.Block(TestBlockSink()) - self.sink[1] = self.Block(TestBlockSink()) + """Block with an EltDict of sub-blocks""" - self.nested = ElementDict[ElementDict[Block]]() - self.nested['inner'] = ElementDict[Block]() - self.nested['inner']['a'] = self.Block(TestBlockSink()) - self.nested['inner']['b'] = self.Block(TestBlockSink()) + @override + def contents(self) -> None: + super().contents() + self.sink = ElementDict[Block]() + self.sink[0] = self.Block(TestBlockSink()) + self.sink[1] = self.Block(TestBlockSink()) + + self.nested = ElementDict[ElementDict[Block]]() + self.nested["inner"] = ElementDict[Block]() + self.nested["inner"]["a"] = self.Block(TestBlockSink()) + self.nested["inner"]["b"] = self.Block(TestBlockSink()) class EltDictBlockProtoTestCase(unittest.TestCase): - def test_connectivity(self) -> None: - pb = EltDictBlock()._elaborated_def_to_proto() - self.assertEqual(pb.blocks[0].name, 'sink[0]') - self.assertEqual(pb.blocks[0].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") - self.assertEqual(pb.blocks[1].name, 'sink[1]') - self.assertEqual(pb.blocks[1].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") - self.assertEqual(pb.blocks[2].name, 'nested[inner][a]') - self.assertEqual(pb.blocks[2].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") - self.assertEqual(pb.blocks[3].name, 'nested[inner][b]') - self.assertEqual(pb.blocks[3].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") + def test_connectivity(self) -> None: + pb = EltDictBlock()._elaborated_def_to_proto() + self.assertEqual(pb.blocks[0].name, "sink[0]") + self.assertEqual(pb.blocks[0].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") + self.assertEqual(pb.blocks[1].name, "sink[1]") + self.assertEqual(pb.blocks[1].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") + self.assertEqual(pb.blocks[2].name, "nested[inner][a]") + self.assertEqual(pb.blocks[2].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") + self.assertEqual(pb.blocks[3].name, "nested[inner][b]") + self.assertEqual(pb.blocks[3].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") diff --git a/edg/core/test_block_errors.py b/edg/core/test_block_errors.py index 3efd4148b..b853f421c 100644 --- a/edg/core/test_block_errors.py +++ b/edg/core/test_block_errors.py @@ -8,66 +8,72 @@ class BadLinkTestCase(unittest.TestCase): - # This needs to be an internal class to avoid this error case being auto-discovered in a library - - class OverconnectedHierarchyBlock(Block): - """A block with connections that don't fit the link (2 sources connected vs. one in the link)""" - @override - def contents(self) -> None: - super().contents() - self.source1 = self.Block(TestBlockSource()) - self.source2 = self.Block(TestBlockSource()) - self.sink = self.Block(TestBlockSink()) - self.test_net = self.connect(self.source1.source, self.sink.sink) - self.connect(self.source1.source, self.source2.source) - assert False # the above connect should error (providing a useful traceback), should not reach this statement - - def test_overconnected_link(self) -> None: - with self.assertRaises(UnconnectableError): - self.OverconnectedHierarchyBlock()._elaborated_def_to_proto() - - class NoBridgeHierarchyBlock(Block): - """A block where a bridge can't be inferred (TestPortSource has no bridge)""" - def __init__(self) -> None: - super().__init__() - self.source_port = self.Port(TestPortSource()) - - @override - def contents(self) -> None: - super().contents() - self.source = self.Block(TestBlockSource()) - self.sink = self.Block(TestBlockSink()) - self.test_net = self.connect(self.source_port, self.source.source, self.sink.sink) - assert False # the above connect should error - - def test_no_bridge_link(self) -> None: - with self.assertRaises(UnconnectableError): - self.NoBridgeHierarchyBlock()._elaborated_def_to_proto() - - class UnboundHierarchyBlock(Block): - """A block that uses a port that isn't bound through self.Port(...)""" - def __init__(self) -> None: - super().__init__() - unbound_port = TestPortSource() - self.test_net = self.connect(unbound_port) - assert False # the above connect should error - - def test_unbound_link(self) -> None: - with self.assertRaises(UnconnectableError): - self.NoBridgeHierarchyBlock()._elaborated_def_to_proto() - - class AmbiguousJoinBlock(Block): - """A block with a connect join that merges two names""" - @override - def contents(self) -> None: - super().contents() - self.source = self.Block(TestBlockSource()) - self.sink1 = self.Block(TestBlockSink()) - self.sink2 = self.Block(TestBlockSink()) - self.test_net1 = self.connect(self.source.source, self.sink1.sink) - self.test_net2 = self.connect(self.sink2.sink) - self.connect(self.test_net1, self.test_net2) - - def test_ambiguous_join(self) -> None: - with self.assertRaises(UnconnectableError): - self.AmbiguousJoinBlock()._elaborated_def_to_proto() + # This needs to be an internal class to avoid this error case being auto-discovered in a library + + class OverconnectedHierarchyBlock(Block): + """A block with connections that don't fit the link (2 sources connected vs. one in the link)""" + + @override + def contents(self) -> None: + super().contents() + self.source1 = self.Block(TestBlockSource()) + self.source2 = self.Block(TestBlockSource()) + self.sink = self.Block(TestBlockSink()) + self.test_net = self.connect(self.source1.source, self.sink.sink) + self.connect(self.source1.source, self.source2.source) + assert ( + False # the above connect should error (providing a useful traceback), should not reach this statement + ) + + def test_overconnected_link(self) -> None: + with self.assertRaises(UnconnectableError): + self.OverconnectedHierarchyBlock()._elaborated_def_to_proto() + + class NoBridgeHierarchyBlock(Block): + """A block where a bridge can't be inferred (TestPortSource has no bridge)""" + + def __init__(self) -> None: + super().__init__() + self.source_port = self.Port(TestPortSource()) + + @override + def contents(self) -> None: + super().contents() + self.source = self.Block(TestBlockSource()) + self.sink = self.Block(TestBlockSink()) + self.test_net = self.connect(self.source_port, self.source.source, self.sink.sink) + assert False # the above connect should error + + def test_no_bridge_link(self) -> None: + with self.assertRaises(UnconnectableError): + self.NoBridgeHierarchyBlock()._elaborated_def_to_proto() + + class UnboundHierarchyBlock(Block): + """A block that uses a port that isn't bound through self.Port(...)""" + + def __init__(self) -> None: + super().__init__() + unbound_port = TestPortSource() + self.test_net = self.connect(unbound_port) + assert False # the above connect should error + + def test_unbound_link(self) -> None: + with self.assertRaises(UnconnectableError): + self.NoBridgeHierarchyBlock()._elaborated_def_to_proto() + + class AmbiguousJoinBlock(Block): + """A block with a connect join that merges two names""" + + @override + def contents(self) -> None: + super().contents() + self.source = self.Block(TestBlockSource()) + self.sink1 = self.Block(TestBlockSink()) + self.sink2 = self.Block(TestBlockSink()) + self.test_net1 = self.connect(self.source.source, self.sink1.sink) + self.test_net2 = self.connect(self.sink2.sink) + self.connect(self.test_net1, self.test_net2) + + def test_ambiguous_join(self) -> None: + with self.assertRaises(UnconnectableError): + self.AmbiguousJoinBlock()._elaborated_def_to_proto() diff --git a/edg/core/test_block_portvector.py b/edg/core/test_block_portvector.py index 17a579671..469acd2d7 100644 --- a/edg/core/test_block_portvector.py +++ b/edg/core/test_block_portvector.py @@ -9,223 +9,229 @@ @abstract_block class TestBlockPortVectorBase(Block): - def __init__(self) -> None: - super().__init__() - self.vector = self.Port(Vector(TestPortSink()), optional=True) # avoid required constraint + def __init__(self) -> None: + super().__init__() + self.vector = self.Port(Vector(TestPortSink()), optional=True) # avoid required constraint class TestBlockPortVectorConcrete(TestBlockPortVectorBase): - def __init__(self) -> None: - super().__init__() - self.vector.append_elt(TestPortSink()) - self.vector.append_elt(TestPortSink()) + def __init__(self) -> None: + super().__init__() + self.vector.append_elt(TestPortSink()) + self.vector.append_elt(TestPortSink()) class TestBlockPortVectorEmpty(TestBlockPortVectorBase): - def __init__(self) -> None: - super().__init__() - self.vector.defined() + def __init__(self) -> None: + super().__init__() + self.vector.defined() class TestBlockPortVectorExport(TestBlockPortVectorBase): - def __init__(self) -> None: - super().__init__() - vector0 = self.vector.append_elt(TestPortSink()) - vector1 = self.vector.append_elt(TestPortSink()) - self.block0 = self.Block(TestBlockSink()) - self.exported0 = self.connect(self.block0.sink, vector0) - self.block1 = self.Block(TestBlockSink()) - self.exported1 = self.connect(self.block1.sink, vector1) + def __init__(self) -> None: + super().__init__() + vector0 = self.vector.append_elt(TestPortSink()) + vector1 = self.vector.append_elt(TestPortSink()) + self.block0 = self.Block(TestBlockSink()) + self.exported0 = self.connect(self.block0.sink, vector0) + self.block1 = self.Block(TestBlockSink()) + self.exported1 = self.connect(self.block1.sink, vector1) class TestBlockPortVectorBridged(TestBlockPortVectorBase): - def __init__(self) -> None: - super().__init__() - vector_port = self.vector.append_elt(TestPortSink()) - self.block0 = self.Block(TestBlockSink()) - self.block1 = self.Block(TestBlockSink()) - self.conn = self.connect(self.block0.sink, self.block1.sink, vector_port) + def __init__(self) -> None: + super().__init__() + vector_port = self.vector.append_elt(TestPortSink()) + self.block0 = self.Block(TestBlockSink()) + self.block1 = self.Block(TestBlockSink()) + self.conn = self.connect(self.block0.sink, self.block1.sink, vector_port) class TestBlockPortVectorConnect(Block): - def __init__(self) -> None: - super().__init__() - self.sink = self.Block(TestBlockPortVectorBase()) - self.source0 = self.Block(TestBlockSource()) - self.source1 = self.Block(TestBlockSource()) - self.conn0 = self.connect(self.source0.source, self.sink.vector.request()) - self.conn1 = self.connect(self.source1.source, self.sink.vector.request()) + def __init__(self) -> None: + super().__init__() + self.sink = self.Block(TestBlockPortVectorBase()) + self.source0 = self.Block(TestBlockSource()) + self.source1 = self.Block(TestBlockSource()) + self.conn0 = self.connect(self.source0.source, self.sink.vector.request()) + self.conn1 = self.connect(self.source1.source, self.sink.vector.request()) class TestBlockPortVectorConstraint(TestBlockPortVectorBase): - def __init__(self) -> None: - super().__init__() - # check reduction ops work on block-side as well - self.float_param_sink_sum = self.Parameter(FloatExpr(self.vector.sum(lambda p: p.float_param))) + def __init__(self) -> None: + super().__init__() + # check reduction ops work on block-side as well + self.float_param_sink_sum = self.Parameter(FloatExpr(self.vector.sum(lambda p: p.float_param))) class BlockVectorBaseProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockPortVectorBase()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = TestBlockPortVectorBase()._elaborated_def_to_proto() - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 1) - self.assertEqual(self.pb.ports[0].name, 'vector') - self.assertEqual(self.pb.ports[0].value.array.self_class.target.name, "edg.core.test_elaboration_common.TestPortSink") + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 1) + self.assertEqual(self.pb.ports[0].name, "vector") + self.assertEqual( + self.pb.ports[0].value.array.self_class.target.name, "edg.core.test_elaboration_common.TestPortSink" + ) - def test_port_init(self) -> None: - self.assertEqual(len(self.pb.constraints), 0) # no constraints should generate + def test_port_init(self) -> None: + self.assertEqual(len(self.pb.constraints), 0) # no constraints should generate class BlockVectorProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockPortVectorConcrete()._elaborated_def_to_proto() - - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 1) - self.assertEqual(self.pb.ports[0].name, 'vector') - self.assertEqual(self.pb.ports[0].value.array.self_class.target.name, "edg.core.test_elaboration_common.TestPortSink") - array_ports = self.pb.ports[0].value.array.ports.ports - self.assertEqual(len(array_ports), 2) - self.assertEqual(array_ports[0].name, '0') - self.assertEqual(array_ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") - self.assertEqual(array_ports[1].name, '1') - self.assertEqual(array_ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") + @override + def setUp(self) -> None: + self.pb = TestBlockPortVectorConcrete()._elaborated_def_to_proto() + + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 1) + self.assertEqual(self.pb.ports[0].name, "vector") + self.assertEqual( + self.pb.ports[0].value.array.self_class.target.name, "edg.core.test_elaboration_common.TestPortSink" + ) + array_ports = self.pb.ports[0].value.array.ports.ports + self.assertEqual(len(array_ports), 2) + self.assertEqual(array_ports[0].name, "0") + self.assertEqual(array_ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") + self.assertEqual(array_ports[1].name, "1") + self.assertEqual(array_ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") class BlockVectorEmptyProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockPortVectorEmpty()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = TestBlockPortVectorEmpty()._elaborated_def_to_proto() - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 1) - self.assertEqual(self.pb.ports[0].value.array.self_class.target.name, "edg.core.test_elaboration_common.TestPortSink") - self.assertTrue(self.pb.ports[0].value.array.HasField('ports')) - array_ports = self.pb.ports[0].value.array.ports.ports - self.assertEqual(len(array_ports), 0) + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 1) + self.assertEqual( + self.pb.ports[0].value.array.self_class.target.name, "edg.core.test_elaboration_common.TestPortSink" + ) + self.assertTrue(self.pb.ports[0].value.array.HasField("ports")) + array_ports = self.pb.ports[0].value.array.ports.ports + self.assertEqual(len(array_ports), 0) class VectorExportProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockPortVectorExport()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = TestBlockPortVectorExport()._elaborated_def_to_proto() - def test_export(self) -> None: - self.assertEqual(len(self.pb.constraints), 2) - constraints = list(map(lambda x: x.value, self.pb.constraints)) + def test_export(self) -> None: + self.assertEqual(len(self.pb.constraints), 2) + constraints = list(map(lambda x: x.value, self.pb.constraints)) - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'vector' - expected_conn.exported.exterior_port.ref.steps.add().name = '0' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'block0' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "vector" + expected_conn.exported.exterior_port.ref.steps.add().name = "0" + expected_conn.exported.internal_block_port.ref.steps.add().name = "block0" + expected_conn.exported.internal_block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'vector' - expected_conn.exported.exterior_port.ref.steps.add().name = '1' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'block1' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "vector" + expected_conn.exported.exterior_port.ref.steps.add().name = "1" + expected_conn.exported.internal_block_port.ref.steps.add().name = "block1" + expected_conn.exported.internal_block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) class VectorBridgedProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockPortVectorBridged()._elaborated_def_to_proto() - - def test_bridged(self) -> None: - self.assertEqual(len(self.pb.constraints), 4) - constraints = list(map(lambda x: x.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'vector' - expected_conn.exported.exterior_port.ref.steps.add().name = '0' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(bridge)vector.0' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'outer_port' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = '(bridge)vector.0' - expected_conn.connected.block_port.ref.steps.add().name = 'inner_link' - expected_conn.connected.link_port.ref.steps.add().name = 'conn' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'block0' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expected_conn.connected.link_port.ref.steps.add().name = 'conn' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'block1' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expected_conn.connected.link_port.ref.steps.add().name = 'conn' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = TestBlockPortVectorBridged()._elaborated_def_to_proto() + + def test_bridged(self) -> None: + self.assertEqual(len(self.pb.constraints), 4) + constraints = list(map(lambda x: x.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "vector" + expected_conn.exported.exterior_port.ref.steps.add().name = "0" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(bridge)vector.0" + expected_conn.exported.internal_block_port.ref.steps.add().name = "outer_port" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "(bridge)vector.0" + expected_conn.connected.block_port.ref.steps.add().name = "inner_link" + expected_conn.connected.link_port.ref.steps.add().name = "conn" + expected_conn.connected.link_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "block0" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expected_conn.connected.link_port.ref.steps.add().name = "conn" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "block1" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expected_conn.connected.link_port.ref.steps.add().name = "conn" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) class VectorConnectProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockPortVectorConnect()._elaborated_def_to_proto() - - def test_export(self) -> None: - self.assertEqual(len(self.pb.constraints), 4) - constraints = list(map(lambda x: x.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expected_conn.connected.block_port.ref.steps.add().name = 'vector' - expected_conn.connected.block_port.ref.steps.add().allocate = '' - expected_conn.connected.link_port.ref.steps.add().name = 'conn0' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'source0' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - expected_conn.connected.link_port.ref.steps.add().name = 'conn0' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expected_conn.connected.block_port.ref.steps.add().name = 'vector' - expected_conn.connected.block_port.ref.steps.add().allocate = '' - expected_conn.connected.link_port.ref.steps.add().name = 'conn1' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'source1' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - expected_conn.connected.link_port.ref.steps.add().name = 'conn1' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = TestBlockPortVectorConnect()._elaborated_def_to_proto() + + def test_export(self) -> None: + self.assertEqual(len(self.pb.constraints), 4) + constraints = list(map(lambda x: x.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expected_conn.connected.block_port.ref.steps.add().name = "vector" + expected_conn.connected.block_port.ref.steps.add().allocate = "" + expected_conn.connected.link_port.ref.steps.add().name = "conn0" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "source0" + expected_conn.connected.block_port.ref.steps.add().name = "source" + expected_conn.connected.link_port.ref.steps.add().name = "conn0" + expected_conn.connected.link_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expected_conn.connected.block_port.ref.steps.add().name = "vector" + expected_conn.connected.block_port.ref.steps.add().allocate = "" + expected_conn.connected.link_port.ref.steps.add().name = "conn1" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "source1" + expected_conn.connected.block_port.ref.steps.add().name = "source" + expected_conn.connected.link_port.ref.steps.add().name = "conn1" + expected_conn.connected.link_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) class VectorConstraintProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBlockPortVectorConstraint()._elaborated_def_to_proto() - - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.constraints), 1) - constraints = list(map(lambda x: x.value, self.pb.constraints)) - - expected_constr = edgir.ValueExpr() - expected_constr.assign.dst.steps.add().name = 'float_param_sink_sum' - expected_constr.assign.src.unary_set.op = edgir.UnarySetExpr.SUM - expected_constr.assign.src.unary_set.vals.map_extract.container.ref.steps.add().name = 'vector' - expected_constr.assign.src.unary_set.vals.map_extract.path.steps.add().name = 'float_param' - self.assertIn(expected_constr, constraints) + @override + def setUp(self) -> None: + self.pb = TestBlockPortVectorConstraint()._elaborated_def_to_proto() + + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.constraints), 1) + constraints = list(map(lambda x: x.value, self.pb.constraints)) + + expected_constr = edgir.ValueExpr() + expected_constr.assign.dst.steps.add().name = "float_param_sink_sum" + expected_constr.assign.src.unary_set.op = edgir.UnarySetExpr.SUM + expected_constr.assign.src.unary_set.vals.map_extract.container.ref.steps.add().name = "vector" + expected_constr.assign.src.unary_set.vals.map_extract.path.steps.add().name = "float_param" + self.assertIn(expected_constr, constraints) diff --git a/edg/core/test_block_prototype.py b/edg/core/test_block_prototype.py index 30689fb71..e22c78a4c 100644 --- a/edg/core/test_block_prototype.py +++ b/edg/core/test_block_prototype.py @@ -4,29 +4,29 @@ class TestBlockPrototype(Block): - def __init__(self) -> None: - super().__init__() - block_model = Block() - assert isinstance(block_model, BlockPrototype) - self.subblock = self.Block(block_model) - assert isinstance(self.subblock, Block) + def __init__(self) -> None: + super().__init__() + block_model = Block() + assert isinstance(block_model, BlockPrototype) + self.subblock = self.Block(block_model) + assert isinstance(self.subblock, Block) class BlockPrototypeTestCase(unittest.TestCase): - def test_args_access(self) -> None: - block = BlockPrototype(Block, ('pos1', 'pos2'), {'k1': 'v1', 'k2': 'v2'}) - assert block._tpe == Block - assert block._args == ('pos1', 'pos2') - assert block._kwargs == {'k1': 'v1', 'k2': 'v2'} + def test_args_access(self) -> None: + block = BlockPrototype(Block, ("pos1", "pos2"), {"k1": "v1", "k2": "v2"}) + assert block._tpe == Block + assert block._args == ("pos1", "pos2") + assert block._kwargs == {"k1": "v1", "k2": "v2"} - def test_attribute_access(self) -> None: - block = BlockPrototype(Block, (), {}) + def test_attribute_access(self) -> None: + block = BlockPrototype(Block, (), {}) - with self.assertRaises(AttributeError): - block.attr + with self.assertRaises(AttributeError): + block.attr - with self.assertRaises(AttributeError): - block.attr = 2 + with self.assertRaises(AttributeError): + block.attr = 2 - def test_prototype_creation(self) -> None: - TestBlockPrototype() # check that assertions inside fire + def test_prototype_creation(self) -> None: + TestBlockPrototype() # check that assertions inside fire diff --git a/edg/core/test_bufferserializer.py b/edg/core/test_bufferserializer.py index ffc74ec59..30224a922 100644 --- a/edg/core/test_bufferserializer.py +++ b/edg/core/test_bufferserializer.py @@ -5,62 +5,62 @@ class BufferSerializerTestCase(unittest.TestCase): - def test_serialize(self) -> None: - pb1 = edgir.ValueLit() - pb1.range.minimum.floating.val = 42 - pb1.range.maximum.floating.val = 127.8 - pb2 = edgir.ValueLit() - pb2.text.val = "!" * 200 + def test_serialize(self) -> None: + pb1 = edgir.ValueLit() + pb1.range.minimum.floating.val = 42 + pb1.range.maximum.floating.val = 127.8 + pb2 = edgir.ValueLit() + pb2.text.val = "!" * 200 - buffer = io.BytesIO() - serializer = BufferSerializer[edgir.ValueLit](buffer) - serializer.write(pb1) - serializer.write(pb2) + buffer = io.BytesIO() + serializer = BufferSerializer[edgir.ValueLit](buffer) + serializer.write(pb1) + serializer.write(pb2) - # assume small enough so size is only one byte - assert pb1.ByteSize() <= 127 - assert 127 < pb2.ByteSize() <= 255 + # assume small enough so size is only one byte + assert pb1.ByteSize() <= 127 + assert 127 < pb2.ByteSize() <= 255 - bytes = buffer.getbuffer().tobytes() - self.assertEqual(len(bytes), 1 + 1 + len(pb1.SerializeToString()) + 1 + 2 + len(pb2.SerializeToString())) + bytes = buffer.getbuffer().tobytes() + self.assertEqual(len(bytes), 1 + 1 + len(pb1.SerializeToString()) + 1 + 2 + len(pb2.SerializeToString())) - pb2pos = 2 + len(pb1.SerializeToString()) - pb2len = len(pb2.SerializeToString()) - self.assertEqual(bytes[0], 0xfe) - self.assertEqual(bytes[1], len(pb1.SerializeToString())) - self.assertEqual(bytes[2:pb2pos], pb1.SerializeToString()) - self.assertEqual(bytes[pb2pos], 0xfe) - self.assertEqual(bytes[pb2pos + 1], pb2len & 0x7f | 0x80) - self.assertEqual(bytes[pb2pos + 2], pb2len >> 7 & 0x7f) - self.assertEqual(bytes[pb2pos + 3:], pb2.SerializeToString()) + pb2pos = 2 + len(pb1.SerializeToString()) + pb2len = len(pb2.SerializeToString()) + self.assertEqual(bytes[0], 0xFE) + self.assertEqual(bytes[1], len(pb1.SerializeToString())) + self.assertEqual(bytes[2:pb2pos], pb1.SerializeToString()) + self.assertEqual(bytes[pb2pos], 0xFE) + self.assertEqual(bytes[pb2pos + 1], pb2len & 0x7F | 0x80) + self.assertEqual(bytes[pb2pos + 2], pb2len >> 7 & 0x7F) + self.assertEqual(bytes[pb2pos + 3 :], pb2.SerializeToString()) class BufferDeserializerTestCase(unittest.TestCase): - def test_deserialize(self) -> None: - pb1 = edgir.ValueLit() - pb1.range.minimum.floating.val = 42 - pb1.range.maximum.floating.val = 127.8 - pb2 = edgir.ValueLit() - pb2.text.val = "!" * 200 + def test_deserialize(self) -> None: + pb1 = edgir.ValueLit() + pb1.range.minimum.floating.val = 42 + pb1.range.maximum.floating.val = 127.8 + pb2 = edgir.ValueLit() + pb2.text.val = "!" * 200 - # assume small enough so size is only one byte - assert pb1.ByteSize() <= 127 - assert 127 < pb2.ByteSize() <= 255 + # assume small enough so size is only one byte + assert pb1.ByteSize() <= 127 + assert 127 < pb2.ByteSize() <= 255 - buffer = io.BytesIO(b'\x00' * (1 + 1 + len(pb1.SerializeToString()) + 1 + 2 + len(pb2.SerializeToString()))) - bytes = buffer.getbuffer() - pb2pos = 1 + 1 + len(pb1.SerializeToString()) - pb2len = len(pb2.SerializeToString()) - bytes[0] = 0xfe - bytes[1] = len(pb1.SerializeToString()) - bytes[2:pb2pos] = pb1.SerializeToString() - bytes[pb2pos] = 0xfe - bytes[pb2pos + 1] = pb2len & 0x7f | 0x80 - bytes[pb2pos + 2] = pb2len >> 7 & 0x7f - bytes[pb2pos + 3:] = pb2.SerializeToString() + buffer = io.BytesIO(b"\x00" * (1 + 1 + len(pb1.SerializeToString()) + 1 + 2 + len(pb2.SerializeToString()))) + bytes = buffer.getbuffer() + pb2pos = 1 + 1 + len(pb1.SerializeToString()) + pb2len = len(pb2.SerializeToString()) + bytes[0] = 0xFE + bytes[1] = len(pb1.SerializeToString()) + bytes[2:pb2pos] = pb1.SerializeToString() + bytes[pb2pos] = 0xFE + bytes[pb2pos + 1] = pb2len & 0x7F | 0x80 + bytes[pb2pos + 2] = pb2len >> 7 & 0x7F + bytes[pb2pos + 3 :] = pb2.SerializeToString() - deserializer = BufferDeserializer(edgir.ValueLit, buffer) - pb_deserialized = deserializer.read() - self.assertEqual(pb_deserialized, pb1) - pb_deserialized = deserializer.read() - self.assertEqual(pb_deserialized, pb2) + deserializer = BufferDeserializer(edgir.ValueLit, buffer) + pb_deserialized = deserializer.read() + self.assertEqual(pb_deserialized, pb1) + pb_deserialized = deserializer.read() + self.assertEqual(pb_deserialized, pb2) diff --git a/edg/core/test_bundle.py b/edg/core/test_bundle.py index fe457b16b..57e87b68d 100644 --- a/edg/core/test_bundle.py +++ b/edg/core/test_bundle.py @@ -7,29 +7,33 @@ class TestBundle(Bundle): - def __init__(self, float_param: FloatLike = FloatExpr(), - a_float_param: FloatLike = FloatExpr(), b_float_param: FloatLike = FloatExpr()) -> None: - super().__init__() + def __init__( + self, + float_param: FloatLike = FloatExpr(), + a_float_param: FloatLike = FloatExpr(), + b_float_param: FloatLike = FloatExpr(), + ) -> None: + super().__init__() - self.float_param = self.Parameter(FloatExpr(float_param)) + self.float_param = self.Parameter(FloatExpr(float_param)) - self.a = self.Port(TestPortSink(float_param=a_float_param)) - self.b = self.Port(TestPortSink(float_param=b_float_param)) + self.a = self.Port(TestPortSink(float_param=a_float_param)) + self.b = self.Port(TestPortSink(float_param=b_float_param)) class BundleProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBundle()._def_to_proto() - - def test_contains_param(self) -> None: - self.assertEqual(len(self.pb.params), 1) - self.assertEqual(self.pb.params[0].name, 'float_param') - self.assertTrue(self.pb.params[0].value.HasField('floating')) - - def test_contains_field(self) -> None: - self.assertEqual(len(self.pb.ports), 2) - self.assertEqual(self.pb.ports[0].name, 'a') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") - self.assertEqual(self.pb.ports[1].name, 'b') - self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") + @override + def setUp(self) -> None: + self.pb = TestBundle()._def_to_proto() + + def test_contains_param(self) -> None: + self.assertEqual(len(self.pb.params), 1) + self.assertEqual(self.pb.params[0].name, "float_param") + self.assertTrue(self.pb.params[0].value.HasField("floating")) + + def test_contains_field(self) -> None: + self.assertEqual(len(self.pb.ports), 2) + self.assertEqual(self.pb.ports[0].name, "a") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") + self.assertEqual(self.pb.ports[1].name, "b") + self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") diff --git a/edg/core/test_chain_connect.py b/edg/core/test_chain_connect.py index 11e0f43be..c1f5c28a2 100644 --- a/edg/core/test_chain_connect.py +++ b/edg/core/test_chain_connect.py @@ -8,108 +8,105 @@ class TestBlockChainOutput(Block): - def __init__(self) -> None: - super().__init__() - self.source = self.Port(TestPortSource(), [Output]) + def __init__(self) -> None: + super().__init__() + self.source = self.Port(TestPortSource(), [Output]) class TestBlockChainInOut(Block): - def __init__(self) -> None: - super().__init__() - self.sink = self.Port(TestPortSink(), [InOut]) + def __init__(self) -> None: + super().__init__() + self.sink = self.Port(TestPortSink(), [InOut]) class TestBlockChainInputOutput(Block): - def __init__(self) -> None: - super().__init__() - self.sink = self.Port(TestPortSink(), [Input]) - self.source = self.Port(TestPortSource(), [Output]) + def __init__(self) -> None: + super().__init__() + self.sink = self.Port(TestPortSink(), [Input]) + self.source = self.Port(TestPortSource(), [Output]) class TestBlockChainInput(Block): - def __init__(self) -> None: - super().__init__() - self.sink = self.Port(TestPortSink(), [Input]) + def __init__(self) -> None: + super().__init__() + self.sink = self.Port(TestPortSink(), [Input]) class ChainConnectUnpackNamedBlock(Block): - """Block with chain connect, where those blocks are declared inline and named by unpack""" - @override - def contents(self) -> None: - super().contents() - (self.output, self.inout, self.inputoutput, self.input), self.test_chain = self.chain( - self.Block(TestBlockChainOutput()), - self.Block(TestBlockChainInOut()), - self.Block(TestBlockChainInputOutput()), - self.Block(TestBlockChainInput()) - ) + """Block with chain connect, where those blocks are declared inline and named by unpack""" + + @override + def contents(self) -> None: + super().contents() + (self.output, self.inout, self.inputoutput, self.input), self.test_chain = self.chain( + self.Block(TestBlockChainOutput()), + self.Block(TestBlockChainInOut()), + self.Block(TestBlockChainInputOutput()), + self.Block(TestBlockChainInput()), + ) class ChainConnectExplicitNamedBlock(Block): - """Block with chain connect, where those blocks are explicitly (separately) declared and named""" - @override - def contents(self) -> None: - super().contents() - self.output = self.Block(TestBlockChainOutput()) - self.inout = self.Block(TestBlockChainInOut()) - self.inputoutput = self.Block(TestBlockChainInputOutput()) - self.input = self.Block(TestBlockChainInput()) - - self.test_chain = self.chain( - self.output, - self.inout, - self.inputoutput, - self.input - ) + """Block with chain connect, where those blocks are explicitly (separately) declared and named""" + + @override + def contents(self) -> None: + super().contents() + self.output = self.Block(TestBlockChainOutput()) + self.inout = self.Block(TestBlockChainInOut()) + self.inputoutput = self.Block(TestBlockChainInputOutput()) + self.input = self.Block(TestBlockChainInput()) + + self.test_chain = self.chain(self.output, self.inout, self.inputoutput, self.input) class ImplicitConnectTestCase(unittest.TestCase): - def verify_pb(self, pb: edgir.HierarchyBlock) -> None: - self.assertEqual(len(pb.constraints), 5) # including source export, connect, port required - constraints = list(map(lambda pair: pair.value, pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_chain_0' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'output' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_chain_0' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'inout' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_chain_0' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'inputoutput' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_chain_1' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'inputoutput' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_chain_1' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'input' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - def test_explicit_named_chain(self) -> None: - pb = ChainConnectExplicitNamedBlock()._elaborated_def_to_proto() - self.verify_pb(pb) - - def test_unpack_named_chain(self) -> None: - pb = ChainConnectUnpackNamedBlock()._elaborated_def_to_proto() - self.verify_pb(pb) + def verify_pb(self, pb: edgir.HierarchyBlock) -> None: + self.assertEqual(len(pb.constraints), 5) # including source export, connect, port required + constraints = list(map(lambda pair: pair.value, pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_chain_0" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "output" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_chain_0" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "inout" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_chain_0" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "inputoutput" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_chain_1" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "inputoutput" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_chain_1" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "input" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + def test_explicit_named_chain(self) -> None: + pb = ChainConnectExplicitNamedBlock()._elaborated_def_to_proto() + self.verify_pb(pb) + + def test_unpack_named_chain(self) -> None: + pb = ChainConnectUnpackNamedBlock()._elaborated_def_to_proto() + self.verify_pb(pb) diff --git a/edg/core/test_common.py b/edg/core/test_common.py index 4d282cbd8..e51cae78b 100644 --- a/edg/core/test_common.py +++ b/edg/core/test_common.py @@ -9,51 +9,51 @@ class TestLink(Link): - def __init__(self) -> None: - super().__init__() - self.source = self.Port(TestPortSource(), optional=True) - self.sinks = self.Port(Vector(TestPortSink()), optional=True) + def __init__(self) -> None: + super().__init__() + self.source = self.Port(TestPortSource(), optional=True) + self.sinks = self.Port(Vector(TestPortSink()), optional=True) class TestPortBase(Port[TestLink]): - link_type = TestLink + link_type = TestLink class TestPortSource(TestPortBase): - pass + pass class TestPortBridge(PortBridge): - def __init__(self) -> None: - super().__init__() - self.outer_port = self.Port(TestPortSink()) - self.inner_link = self.Port(TestPortSource()) + def __init__(self) -> None: + super().__init__() + self.outer_port = self.Port(TestPortSink()) + self.inner_link = self.Port(TestPortSource()) class TestPortSink(TestPortBase): - bridge_type = TestPortBridge + bridge_type = TestPortBridge class TestBlockSink(Block): - def __init__(self) -> None: - super().__init__() - self.sink = self.Port(TestPortSink(), optional=True) + def __init__(self) -> None: + super().__init__() + self.sink = self.Port(TestPortSink(), optional=True) ImplicitSink = PortTag(TestPortSink) class TestBlockImplicitSink(Block): - def __init__(self) -> None: - super().__init__() - self.sink = self.Port(TestPortSink(), [ImplicitSink]) + def __init__(self) -> None: + super().__init__() + self.sink = self.Port(TestPortSink(), [ImplicitSink]) class TestBlockSource(Block): - def __init__(self) -> None: - super().__init__() - self.source = self.Port(TestPortSource(), optional=True) + def __init__(self) -> None: + super().__init__() + self.source = self.Port(TestPortSource(), optional=True) def problem_name_from_module_file(file: str) -> str: - return os.path.splitext(os.path.basename(file))[0] + '.edg' + return os.path.splitext(os.path.basename(file))[0] + ".edg" diff --git a/edg/core/test_connect_array.py b/edg/core/test_connect_array.py index 4a514e887..18ac890f9 100644 --- a/edg/core/test_connect_array.py +++ b/edg/core/test_connect_array.py @@ -8,131 +8,131 @@ class TestBlockSourceFixedArray(Block): - def __init__(self) -> None: - super().__init__() - self.sources = self.Port(Vector(TestPortSource())) - self.sources.append_elt(TestPortSource(), "a") - self.sources.append_elt(TestPortSource(), "b") + def __init__(self) -> None: + super().__init__() + self.sources = self.Port(Vector(TestPortSource())) + self.sources.append_elt(TestPortSource(), "a") + self.sources.append_elt(TestPortSource(), "b") class TestBlockSinkElasticArray(GeneratorBlock): - def __init__(self) -> None: - super().__init__() - self.sinks = self.Port(Vector(TestPortSink())) - self.generator_param(self.sinks.requested()) + def __init__(self) -> None: + super().__init__() + self.sinks = self.Port(Vector(TestPortSink())) + self.generator_param(self.sinks.requested()) - @override - def generate(self) -> None: - super().generate() - for request in self.get(self.sinks.requested()): - self.sinks.append_elt(TestPortSink(), request) + @override + def generate(self) -> None: + super().generate() + for request in self.get(self.sinks.requested()): + self.sinks.append_elt(TestPortSink(), request) class ArrayConnectBlock(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestBlockSourceFixedArray()) - self.sink1 = self.Block(TestBlockSinkElasticArray()) - self.sink2 = self.Block(TestBlockSinkElasticArray()) - self.test_net = self.connect(self.source.sources, self.sink1.sinks, self.sink2.sinks) + self.source = self.Block(TestBlockSourceFixedArray()) + self.sink1 = self.Block(TestBlockSinkElasticArray()) + self.sink2 = self.Block(TestBlockSinkElasticArray()) + self.test_net = self.connect(self.source.sources, self.sink1.sinks, self.sink2.sinks) class ArrayConnectProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = ArrayConnectBlock()._elaborated_def_to_proto() - - def test_link_inference(self) -> None: - self.assertEqual(len(self.pb.links), 1) - self.assertEqual(self.pb.links[0].name, 'test_net') - self.assertEqual(self.pb.links[0].value.array.self_class.target.name, "edg.core.test_common.TestLink") - - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 3) - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connectedArray.link_port.ref.steps.add().name = 'test_net' - expected_conn.connectedArray.link_port.ref.steps.add().name = 'source' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'source' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sources' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connectedArray.link_port.ref.steps.add().name = 'test_net' - expected_conn.connectedArray.link_port.ref.steps.add().name = 'sinks' - expected_conn.connectedArray.link_port.ref.steps.add().allocate = '' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sink1' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sinks' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connectedArray.link_port.ref.steps.add().name = 'test_net' - expected_conn.connectedArray.link_port.ref.steps.add().name = 'sinks' - expected_conn.connectedArray.link_port.ref.steps.add().allocate = '' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sink2' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sinks' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = ArrayConnectBlock()._elaborated_def_to_proto() + + def test_link_inference(self) -> None: + self.assertEqual(len(self.pb.links), 1) + self.assertEqual(self.pb.links[0].name, "test_net") + self.assertEqual(self.pb.links[0].value.array.self_class.target.name, "edg.core.test_common.TestLink") + + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 3) + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connectedArray.link_port.ref.steps.add().name = "test_net" + expected_conn.connectedArray.link_port.ref.steps.add().name = "source" + expected_conn.connectedArray.block_port.ref.steps.add().name = "source" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sources" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connectedArray.link_port.ref.steps.add().name = "test_net" + expected_conn.connectedArray.link_port.ref.steps.add().name = "sinks" + expected_conn.connectedArray.link_port.ref.steps.add().allocate = "" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sink1" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sinks" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connectedArray.link_port.ref.steps.add().name = "test_net" + expected_conn.connectedArray.link_port.ref.steps.add().name = "sinks" + expected_conn.connectedArray.link_port.ref.steps.add().allocate = "" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sink2" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sinks" + self.assertIn(expected_conn, constraints) class ArrayAllocatedConnectBlock(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source1 = self.Block(TestBlockSourceFixedArray()) - self.source2 = self.Block(TestBlockSourceFixedArray()) - self.sink = self.Block(TestBlockSinkElasticArray()) - self.test_net1 = self.connect(self.source1.sources, self.sink.sinks.request_vector()) - self.test_net2 = self.connect(self.source2.sources, self.sink.sinks.request_vector()) + self.source1 = self.Block(TestBlockSourceFixedArray()) + self.source2 = self.Block(TestBlockSourceFixedArray()) + self.sink = self.Block(TestBlockSinkElasticArray()) + self.test_net1 = self.connect(self.source1.sources, self.sink.sinks.request_vector()) + self.test_net2 = self.connect(self.source2.sources, self.sink.sinks.request_vector()) class ArrayAllocatedConnectProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = ArrayAllocatedConnectBlock()._elaborated_def_to_proto() - - def test_link_inference(self) -> None: - self.assertEqual(len(self.pb.links), 2) - self.assertEqual(self.pb.links[0].name, 'test_net1') - self.assertEqual(self.pb.links[0].value.array.self_class.target.name, "edg.core.test_common.TestLink") - self.assertEqual(self.pb.links[1].name, 'test_net2') - self.assertEqual(self.pb.links[1].value.array.self_class.target.name, "edg.core.test_common.TestLink") - - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 4) - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connectedArray.link_port.ref.steps.add().name = 'test_net1' - expected_conn.connectedArray.link_port.ref.steps.add().name = 'source' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'source1' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sources' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connectedArray.link_port.ref.steps.add().name = 'test_net1' - expected_conn.connectedArray.link_port.ref.steps.add().name = 'sinks' - expected_conn.connectedArray.link_port.ref.steps.add().allocate = '' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sink' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sinks' - expected_conn.connectedArray.block_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connectedArray.link_port.ref.steps.add().name = 'test_net2' - expected_conn.connectedArray.link_port.ref.steps.add().name = 'source' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'source2' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sources' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connectedArray.link_port.ref.steps.add().name = 'test_net2' - expected_conn.connectedArray.link_port.ref.steps.add().name = 'sinks' - expected_conn.connectedArray.link_port.ref.steps.add().allocate = '' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sink' - expected_conn.connectedArray.block_port.ref.steps.add().name = 'sinks' - expected_conn.connectedArray.block_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = ArrayAllocatedConnectBlock()._elaborated_def_to_proto() + + def test_link_inference(self) -> None: + self.assertEqual(len(self.pb.links), 2) + self.assertEqual(self.pb.links[0].name, "test_net1") + self.assertEqual(self.pb.links[0].value.array.self_class.target.name, "edg.core.test_common.TestLink") + self.assertEqual(self.pb.links[1].name, "test_net2") + self.assertEqual(self.pb.links[1].value.array.self_class.target.name, "edg.core.test_common.TestLink") + + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 4) + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connectedArray.link_port.ref.steps.add().name = "test_net1" + expected_conn.connectedArray.link_port.ref.steps.add().name = "source" + expected_conn.connectedArray.block_port.ref.steps.add().name = "source1" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sources" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connectedArray.link_port.ref.steps.add().name = "test_net1" + expected_conn.connectedArray.link_port.ref.steps.add().name = "sinks" + expected_conn.connectedArray.link_port.ref.steps.add().allocate = "" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sink" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sinks" + expected_conn.connectedArray.block_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connectedArray.link_port.ref.steps.add().name = "test_net2" + expected_conn.connectedArray.link_port.ref.steps.add().name = "source" + expected_conn.connectedArray.block_port.ref.steps.add().name = "source2" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sources" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connectedArray.link_port.ref.steps.add().name = "test_net2" + expected_conn.connectedArray.link_port.ref.steps.add().name = "sinks" + expected_conn.connectedArray.link_port.ref.steps.add().allocate = "" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sink" + expected_conn.connectedArray.block_port.ref.steps.add().name = "sinks" + expected_conn.connectedArray.block_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) diff --git a/edg/core/test_default.py b/edg/core/test_default.py index c097eda82..c4ecc3e74 100644 --- a/edg/core/test_default.py +++ b/edg/core/test_default.py @@ -6,75 +6,76 @@ class BaseParamClass(Block): # Base class containing no parameters or default - pass + pass class NonDefaultParamClass(BaseParamClass): # contains a single param without a default - def __init__(self, nondefault_param: IntLike) -> None: - super().__init__() + def __init__(self, nondefault_param: IntLike) -> None: + super().__init__() class NonDefaultParamSubClass(BaseParamClass): # inherits defaults without adding anything - pass + pass class EmptyDefaultParamClass(BaseParamClass): # contains a single empty-default param - def __init__(self, nondefault_param: IntLike = IntExpr()) -> None: - super().__init__() + def __init__(self, nondefault_param: IntLike = IntExpr()) -> None: + super().__init__() class DefaultParamSubClass(EmptyDefaultParamClass): # adds a default param on top of the inherited params - def __init__(self, default_param: IntLike = 42, **kwargs: Any) -> None: - super().__init__(**kwargs) + def __init__(self, default_param: IntLike = 42, **kwargs: Any) -> None: + super().__init__(**kwargs) class OverrideDefaultSubClass(DefaultParamSubClass): # changes the default param of the parent - def __init__(self, default_param: IntLike = 16, **kwargs: Any) -> None: - super().__init__(default_param, **kwargs) + def __init__(self, default_param: IntLike = 16, **kwargs: Any) -> None: + super().__init__(default_param, **kwargs) class CombinedParamSubClass(DefaultParamSubClass): # adds a default param on top of the inherited params - def __init__(self, nondefault_param2: FloatLike = FloatExpr(), - default_param2: StringLike = "test", **kwargs: Any) -> None: - super().__init__(**kwargs) + def __init__( + self, nondefault_param2: FloatLike = FloatExpr(), default_param2: StringLike = "test", **kwargs: Any + ) -> None: + super().__init__(**kwargs) class DefaultTestCase(unittest.TestCase): - def test_base(self) -> None: - pb = BaseParamClass()._elaborated_def_to_proto() + def test_base(self) -> None: + pb = BaseParamClass()._elaborated_def_to_proto() - self.assertEqual(len(pb.param_defaults), 0) + self.assertEqual(len(pb.param_defaults), 0) - def test_non_default(self) -> None: - pb = NonDefaultParamClass()._elaborated_def_to_proto() # type: ignore + def test_non_default(self) -> None: + pb = NonDefaultParamClass()._elaborated_def_to_proto() # type: ignore - self.assertEqual(len(pb.param_defaults), 0) + self.assertEqual(len(pb.param_defaults), 0) - def test_non_default_subclass(self) -> None: - pb = NonDefaultParamSubClass()._elaborated_def_to_proto() + def test_non_default_subclass(self) -> None: + pb = NonDefaultParamSubClass()._elaborated_def_to_proto() - self.assertEqual(len(pb.param_defaults), 0) + self.assertEqual(len(pb.param_defaults), 0) - def test_empty_default(self) -> None: - pb = EmptyDefaultParamClass()._elaborated_def_to_proto() + def test_empty_default(self) -> None: + pb = EmptyDefaultParamClass()._elaborated_def_to_proto() - self.assertEqual(len(pb.param_defaults), 0) + self.assertEqual(len(pb.param_defaults), 0) - def test_default(self) -> None: - pb = DefaultParamSubClass()._elaborated_def_to_proto() + def test_default(self) -> None: + pb = DefaultParamSubClass()._elaborated_def_to_proto() - self.assertEqual(len(pb.param_defaults), 1) - self.assertEqual(pb.param_defaults['default_param'], edgir.lit_to_expr(42)) + self.assertEqual(len(pb.param_defaults), 1) + self.assertEqual(pb.param_defaults["default_param"], edgir.lit_to_expr(42)) - def test_override(self) -> None: - pb = OverrideDefaultSubClass()._elaborated_def_to_proto() + def test_override(self) -> None: + pb = OverrideDefaultSubClass()._elaborated_def_to_proto() - self.assertEqual(len(pb.param_defaults), 1) - self.assertEqual(pb.param_defaults['default_param'], edgir.lit_to_expr(16)) + self.assertEqual(len(pb.param_defaults), 1) + self.assertEqual(pb.param_defaults["default_param"], edgir.lit_to_expr(16)) - def test_combined(self) -> None: - pb = CombinedParamSubClass()._elaborated_def_to_proto() + def test_combined(self) -> None: + pb = CombinedParamSubClass()._elaborated_def_to_proto() - self.assertEqual(len(pb.param_defaults), 2) - self.assertEqual(pb.param_defaults['default_param'], edgir.lit_to_expr(42)) - self.assertEqual(pb.param_defaults['default_param2'], edgir.lit_to_expr("test")) + self.assertEqual(len(pb.param_defaults), 2) + self.assertEqual(pb.param_defaults["default_param"], edgir.lit_to_expr(42)) + self.assertEqual(pb.param_defaults["default_param2"], edgir.lit_to_expr("test")) diff --git a/edg/core/test_design_inst.py b/edg/core/test_design_inst.py index 83019ec98..31edb9e78 100644 --- a/edg/core/test_design_inst.py +++ b/edg/core/test_design_inst.py @@ -7,223 +7,236 @@ class ExportPortHierarchyBlockTop(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(ExportPortHierarchyBlock()) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(ExportPortHierarchyBlock()) class PortBridgeHierarchyBlockTop(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(PortBridgeHierarchyBlock()) - self.source = self.Block(TestBlockSource()) # TODO the PortBridge requires a connect, which isn't satisfied here - self.connect(self.block.source_port, self.source.source) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(PortBridgeHierarchyBlock()) + self.source = self.Block( + TestBlockSource() + ) # TODO the PortBridge requires a connect, which isn't satisfied here + self.connect(self.block.source_port, self.source.source) class DesignInstantiationTestCase(unittest.TestCase): - def test_single_hierarchy(self) -> None: - """ - Tests design instantiation with a single level of hierarchy blocks. - Only tests that the contained blocks are instantiated and structurally correct, does not check internal constraints - """ - compiled_design = ScalaCompiler.compile(TopHierarchyBlock) - pb = compiled_design.contents - - self.assertEqual(pb.self_class.target.name, 'edg.core.test_hierarchy_block.TopHierarchyBlock') - - self.assertEqual(len(pb.blocks), 3) - - self.assertEqual(pb.blocks[0].name, 'source') - self.assertEqual(pb.blocks[0].value.hierarchy.self_class.target.name, - 'edg.core.test_common.TestBlockSource') - self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].name, 'source') - self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].value.port.self_class.target.name, - 'edg.core.test_common.TestPortSource') - - self.assertEqual(pb.blocks[1].name, 'sink1') - self.assertEqual(pb.blocks[1].value.hierarchy.self_class.target.name, - 'edg.core.test_common.TestBlockSink') - self.assertEqual(pb.blocks[1].value.hierarchy.ports[0].name, 'sink') - self.assertEqual(pb.blocks[1].value.hierarchy.ports[0].value.port.self_class.target.name, - 'edg.core.test_common.TestPortSink') - - self.assertEqual(pb.blocks[2].name, 'sink2') - self.assertEqual(pb.blocks[2].value.hierarchy.self_class.target.name, - 'edg.core.test_common.TestBlockSink') - self.assertEqual(pb.blocks[2].value.hierarchy.ports[0].name, 'sink') - self.assertEqual(pb.blocks[2].value.hierarchy.ports[0].value.port.self_class.target.name, - 'edg.core.test_common.TestPortSink') - - self.assertEqual(pb.links[0].name, 'test_net') - self.assertEqual(pb.links[0].value.link.self_class.target.name, 'edg.core.test_common.TestLink') - self.assertEqual(pb.links[0].value.link.ports[0].name, 'source') - self.assertEqual(pb.links[0].value.link.ports[0].value.port.self_class.target.name, - "edg.core.test_common.TestPortSource") - self.assertEqual(pb.links[0].value.link.ports[1].name, 'sinks') - self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[0].name, '0') - self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[0].value.port.self_class.target.name, - "edg.core.test_common.TestPortSink") - self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[1].name, '1') - self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[1].value.port.self_class.target.name, - "edg.core.test_common.TestPortSink") - - constraints = list(map(lambda pair: pair.value, pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink1' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expanded = expected_conn.connected.expanded.add() - expanded.link_port.ref.steps.add().name = 'test_net' - expanded.link_port.ref.steps.add().name = 'sinks' - expanded.link_port.ref.steps.add().name = '0' - expanded.block_port.ref.steps.add().name = 'sink1' - expanded.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink2' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expanded = expected_conn.connected.expanded.add() - expanded.link_port.ref.steps.add().name = 'test_net' - expanded.link_port.ref.steps.add().name = 'sinks' - expanded.link_port.ref.steps.add().name = '1' - expanded.block_port.ref.steps.add().name = 'sink2' - expanded.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - def test_exported_hierarchy(self) -> None: - compiled_design = ScalaCompiler.compile(ExportPortHierarchyBlockTop) - self.assertEqual(compiled_design.contents.blocks[0].name, 'block') - pb = compiled_design.contents.blocks[0].value.hierarchy - - self.assertEqual(pb.self_class.target.name, 'edg.core.test_hierarchy_block.ExportPortHierarchyBlock') - - self.assertEqual(len(pb.ports), 1) - self.assertEqual(pb.ports[0].name, 'exported') - self.assertEqual(pb.ports[0].value.port.self_class.target.name, - 'edg.core.test_common.TestPortSink') - - self.assertEqual(len(pb.blocks), 1) - self.assertEqual(pb.blocks[0].name, 'sink') - self.assertEqual(pb.blocks[0].value.hierarchy.self_class.target.name, - 'edg.core.test_common.TestBlockSink') - self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].name, 'sink') - self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].value.port.self_class.target.name, - 'edg.core.test_common.TestPortSink') - - self.assertEqual(len(pb.links), 0) - - constraints = list(map(lambda pair: pair.value, pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'exported' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'sink' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - def test_bridge_hierarchy(self) -> None: - compiled_design = ScalaCompiler.compile(PortBridgeHierarchyBlockTop) - self.assertEqual(compiled_design.contents.blocks[0].name, 'block') - pb = compiled_design.contents.blocks[0].value.hierarchy - - self.assertEqual(pb.self_class.target.name, 'edg.core.test_hierarchy_block.PortBridgeHierarchyBlock') - - self.assertEqual(len(pb.ports), 1) - self.assertEqual(pb.ports[0].name, 'source_port') - self.assertEqual(pb.ports[0].value.port.self_class.target.name, - "edg.core.test_common.TestPortSink") - - self.assertEqual(len(pb.blocks), 3) - self.assertEqual(pb.blocks[0].name, 'sink1') - self.assertEqual(pb.blocks[0].value.hierarchy.self_class.target.name, - 'edg.core.test_common.TestBlockSink') - self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].name, 'sink') - self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].value.port.self_class.target.name, - 'edg.core.test_common.TestPortSink') - - self.assertEqual(pb.blocks[1].name, 'sink2') - self.assertEqual(pb.blocks[1].value.hierarchy.self_class.target.name, - 'edg.core.test_common.TestBlockSink') - self.assertEqual(pb.blocks[1].value.hierarchy.ports[0].name, 'sink') - self.assertEqual(pb.blocks[1].value.hierarchy.ports[0].value.port.self_class.target.name, - 'edg.core.test_common.TestPortSink') - - self.assertEqual(pb.blocks[2].name, '(bridge)source_port') - self.assertEqual(pb.blocks[2].value.hierarchy.self_class.target.name, - 'edg.core.test_common.TestPortBridge') - self.assertEqual(pb.blocks[2].value.hierarchy.ports[0].name, 'outer_port') - self.assertEqual(pb.blocks[2].value.hierarchy.ports[0].value.port.self_class.target.name, - "edg.core.test_common.TestPortSink") - self.assertEqual(pb.blocks[2].value.hierarchy.ports[1].name, 'inner_link') - self.assertEqual(pb.blocks[2].value.hierarchy.ports[1].value.port.self_class.target.name, - "edg.core.test_common.TestPortSource") - - self.assertEqual(len(pb.links), 1) - self.assertEqual(pb.links[0].name, 'test_net') - self.assertEqual(pb.links[0].value.link.self_class.target.name, 'edg.core.test_common.TestLink') - self.assertEqual(pb.links[0].value.link.ports[0].name, 'source') - self.assertEqual(pb.links[0].value.link.ports[0].value.port.self_class.target.name, - "edg.core.test_common.TestPortSource") - self.assertEqual(pb.links[0].value.link.ports[1].name, 'sinks') - self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[0].name, '0') - self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[0].value.port.self_class.target.name, - "edg.core.test_common.TestPortSink") - self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[1].name, '1') - self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[1].value.port.self_class.target.name, - "edg.core.test_common.TestPortSink") - - constraints = list(map(lambda pair: pair.value, pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = '(bridge)source_port' - expected_conn.connected.block_port.ref.steps.add().name = 'inner_link' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'source_port' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(bridge)source_port' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'outer_port' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink1' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expanded = expected_conn.connected.expanded.add() - expanded.link_port.ref.steps.add().name = 'test_net' - expanded.link_port.ref.steps.add().name = 'sinks' - expanded.link_port.ref.steps.add().name = '0' - expanded.block_port.ref.steps.add().name = 'sink1' - expanded.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink2' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expanded = expected_conn.connected.expanded.add() - expanded.link_port.ref.steps.add().name = 'test_net' - expanded.link_port.ref.steps.add().name = 'sinks' - expanded.link_port.ref.steps.add().name = '1' - expanded.block_port.ref.steps.add().name = 'sink2' - expanded.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + def test_single_hierarchy(self) -> None: + """ + Tests design instantiation with a single level of hierarchy blocks. + Only tests that the contained blocks are instantiated and structurally correct, does not check internal constraints + """ + compiled_design = ScalaCompiler.compile(TopHierarchyBlock) + pb = compiled_design.contents + + self.assertEqual(pb.self_class.target.name, "edg.core.test_hierarchy_block.TopHierarchyBlock") + + self.assertEqual(len(pb.blocks), 3) + + self.assertEqual(pb.blocks[0].name, "source") + self.assertEqual(pb.blocks[0].value.hierarchy.self_class.target.name, "edg.core.test_common.TestBlockSource") + self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].name, "source") + self.assertEqual( + pb.blocks[0].value.hierarchy.ports[0].value.port.self_class.target.name, + "edg.core.test_common.TestPortSource", + ) + + self.assertEqual(pb.blocks[1].name, "sink1") + self.assertEqual(pb.blocks[1].value.hierarchy.self_class.target.name, "edg.core.test_common.TestBlockSink") + self.assertEqual(pb.blocks[1].value.hierarchy.ports[0].name, "sink") + self.assertEqual( + pb.blocks[1].value.hierarchy.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSink" + ) + + self.assertEqual(pb.blocks[2].name, "sink2") + self.assertEqual(pb.blocks[2].value.hierarchy.self_class.target.name, "edg.core.test_common.TestBlockSink") + self.assertEqual(pb.blocks[2].value.hierarchy.ports[0].name, "sink") + self.assertEqual( + pb.blocks[2].value.hierarchy.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSink" + ) + + self.assertEqual(pb.links[0].name, "test_net") + self.assertEqual(pb.links[0].value.link.self_class.target.name, "edg.core.test_common.TestLink") + self.assertEqual(pb.links[0].value.link.ports[0].name, "source") + self.assertEqual( + pb.links[0].value.link.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSource" + ) + self.assertEqual(pb.links[0].value.link.ports[1].name, "sinks") + self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[0].name, "0") + self.assertEqual( + pb.links[0].value.link.ports[1].value.array.ports.ports[0].value.port.self_class.target.name, + "edg.core.test_common.TestPortSink", + ) + self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[1].name, "1") + self.assertEqual( + pb.links[0].value.link.ports[1].value.array.ports.ports[1].value.port.self_class.target.name, + "edg.core.test_common.TestPortSink", + ) + + constraints = list(map(lambda pair: pair.value, pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink1" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expanded = expected_conn.connected.expanded.add() + expanded.link_port.ref.steps.add().name = "test_net" + expanded.link_port.ref.steps.add().name = "sinks" + expanded.link_port.ref.steps.add().name = "0" + expanded.block_port.ref.steps.add().name = "sink1" + expanded.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink2" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expanded = expected_conn.connected.expanded.add() + expanded.link_port.ref.steps.add().name = "test_net" + expanded.link_port.ref.steps.add().name = "sinks" + expanded.link_port.ref.steps.add().name = "1" + expanded.block_port.ref.steps.add().name = "sink2" + expanded.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + def test_exported_hierarchy(self) -> None: + compiled_design = ScalaCompiler.compile(ExportPortHierarchyBlockTop) + self.assertEqual(compiled_design.contents.blocks[0].name, "block") + pb = compiled_design.contents.blocks[0].value.hierarchy + + self.assertEqual(pb.self_class.target.name, "edg.core.test_hierarchy_block.ExportPortHierarchyBlock") + + self.assertEqual(len(pb.ports), 1) + self.assertEqual(pb.ports[0].name, "exported") + self.assertEqual(pb.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSink") + + self.assertEqual(len(pb.blocks), 1) + self.assertEqual(pb.blocks[0].name, "sink") + self.assertEqual(pb.blocks[0].value.hierarchy.self_class.target.name, "edg.core.test_common.TestBlockSink") + self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].name, "sink") + self.assertEqual( + pb.blocks[0].value.hierarchy.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSink" + ) + + self.assertEqual(len(pb.links), 0) + + constraints = list(map(lambda pair: pair.value, pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "exported" + expected_conn.exported.internal_block_port.ref.steps.add().name = "sink" + expected_conn.exported.internal_block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + def test_bridge_hierarchy(self) -> None: + compiled_design = ScalaCompiler.compile(PortBridgeHierarchyBlockTop) + self.assertEqual(compiled_design.contents.blocks[0].name, "block") + pb = compiled_design.contents.blocks[0].value.hierarchy + + self.assertEqual(pb.self_class.target.name, "edg.core.test_hierarchy_block.PortBridgeHierarchyBlock") + + self.assertEqual(len(pb.ports), 1) + self.assertEqual(pb.ports[0].name, "source_port") + self.assertEqual(pb.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSink") + + self.assertEqual(len(pb.blocks), 3) + self.assertEqual(pb.blocks[0].name, "sink1") + self.assertEqual(pb.blocks[0].value.hierarchy.self_class.target.name, "edg.core.test_common.TestBlockSink") + self.assertEqual(pb.blocks[0].value.hierarchy.ports[0].name, "sink") + self.assertEqual( + pb.blocks[0].value.hierarchy.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSink" + ) + + self.assertEqual(pb.blocks[1].name, "sink2") + self.assertEqual(pb.blocks[1].value.hierarchy.self_class.target.name, "edg.core.test_common.TestBlockSink") + self.assertEqual(pb.blocks[1].value.hierarchy.ports[0].name, "sink") + self.assertEqual( + pb.blocks[1].value.hierarchy.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSink" + ) + + self.assertEqual(pb.blocks[2].name, "(bridge)source_port") + self.assertEqual(pb.blocks[2].value.hierarchy.self_class.target.name, "edg.core.test_common.TestPortBridge") + self.assertEqual(pb.blocks[2].value.hierarchy.ports[0].name, "outer_port") + self.assertEqual( + pb.blocks[2].value.hierarchy.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSink" + ) + self.assertEqual(pb.blocks[2].value.hierarchy.ports[1].name, "inner_link") + self.assertEqual( + pb.blocks[2].value.hierarchy.ports[1].value.port.self_class.target.name, + "edg.core.test_common.TestPortSource", + ) + + self.assertEqual(len(pb.links), 1) + self.assertEqual(pb.links[0].name, "test_net") + self.assertEqual(pb.links[0].value.link.self_class.target.name, "edg.core.test_common.TestLink") + self.assertEqual(pb.links[0].value.link.ports[0].name, "source") + self.assertEqual( + pb.links[0].value.link.ports[0].value.port.self_class.target.name, "edg.core.test_common.TestPortSource" + ) + self.assertEqual(pb.links[0].value.link.ports[1].name, "sinks") + self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[0].name, "0") + self.assertEqual( + pb.links[0].value.link.ports[1].value.array.ports.ports[0].value.port.self_class.target.name, + "edg.core.test_common.TestPortSink", + ) + self.assertEqual(pb.links[0].value.link.ports[1].value.array.ports.ports[1].name, "1") + self.assertEqual( + pb.links[0].value.link.ports[1].value.array.ports.ports[1].value.port.self_class.target.name, + "edg.core.test_common.TestPortSink", + ) + + constraints = list(map(lambda pair: pair.value, pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "(bridge)source_port" + expected_conn.connected.block_port.ref.steps.add().name = "inner_link" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "source_port" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(bridge)source_port" + expected_conn.exported.internal_block_port.ref.steps.add().name = "outer_port" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink1" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expanded = expected_conn.connected.expanded.add() + expanded.link_port.ref.steps.add().name = "test_net" + expanded.link_port.ref.steps.add().name = "sinks" + expanded.link_port.ref.steps.add().name = "0" + expanded.block_port.ref.steps.add().name = "sink1" + expanded.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink2" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expanded = expected_conn.connected.expanded.add() + expanded.link_port.ref.steps.add().name = "test_net" + expanded.link_port.ref.steps.add().name = "sinks" + expanded.link_port.ref.steps.add().name = "1" + expanded.block_port.ref.steps.add().name = "sink2" + expanded.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) diff --git a/edg/core/test_elaboration_common.py b/edg/core/test_elaboration_common.py index d4f393672..fa932b12b 100644 --- a/edg/core/test_elaboration_common.py +++ b/edg/core/test_elaboration_common.py @@ -6,6 +6,7 @@ class TestLinkBase(Link): """Inheritance base for TestLink""" + def __init__(self) -> None: super().__init__() @@ -18,8 +19,10 @@ def __init__(self) -> None: self.float_param_sink_sum = self.Parameter(FloatExpr(self.sinks.sum(lambda p: p.float_param))) self.float_param_sink_range = self.Parameter(RangeExpr()) - self.assign(self.float_param_sink_range, - (self.sinks.min(lambda p: p.float_param), self.sinks.max(lambda p: p.float_param))) + self.assign( + self.float_param_sink_range, + (self.sinks.min(lambda p: p.float_param), self.sinks.max(lambda p: p.float_param)), + ) self.range_param_sink_common = self.Parameter(RangeExpr(self.sinks.intersection(lambda p: p.range_limit))) self.require(self.float_param_sink_sum - self.source.float_param == 0) @@ -36,7 +39,12 @@ def __init__(self, float_param: FloatLike = FloatExpr()) -> None: class TestPortSource(TestPortBase): - def __init__(self, float_param_limit: RangeLike = RangeExpr(), range_param: RangeLike = RangeExpr(), float_param: FloatLike = FloatExpr()) -> None: + def __init__( + self, + float_param_limit: RangeLike = RangeExpr(), + range_param: RangeLike = RangeExpr(), + float_param: FloatLike = FloatExpr(), + ) -> None: super().__init__(float_param) self.float_param_limit = self.Parameter(RangeExpr(float_param_limit)) self.range_param = self.Parameter(RangeExpr(range_param)) diff --git a/edg/core/test_generator.py b/edg/core/test_generator.py index 7a0c77922..877e519cc 100644 --- a/edg/core/test_generator.py +++ b/edg/core/test_generator.py @@ -9,208 +9,212 @@ class TestGeneratorAssign(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(GeneratorAssign()) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(GeneratorAssign()) class GeneratorAssign(GeneratorBlock): - def __init__(self) -> None: - super().__init__() - # Because this doesn't have dependency parameters, this is the top-level design - self.float_param = self.Parameter(FloatExpr()) + def __init__(self) -> None: + super().__init__() + # Because this doesn't have dependency parameters, this is the top-level design + self.float_param = self.Parameter(FloatExpr()) - @override - def generate(self) -> None: - self.assign(self.float_param, 2.0) + @override + def generate(self) -> None: + self.assign(self.float_param, 2.0) class TestGeneratorDependency(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(GeneratorDependency(3.0)) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(GeneratorDependency(3.0)) class GeneratorDependency(GeneratorBlock): - def __init__(self, float_preset: FloatLike) -> None: - super().__init__() - self.float_param = self.Parameter(FloatExpr()) - self.float_preset = self.ArgParameter(float_preset) - self.generator_param(self.float_preset) + def __init__(self, float_preset: FloatLike) -> None: + super().__init__() + self.float_param = self.Parameter(FloatExpr()) + self.float_preset = self.ArgParameter(float_preset) + self.generator_param(self.float_preset) - @override - def generate(self) -> None: - self.assign(self.float_param, self.get(self.float_preset) * 2) + @override + def generate(self) -> None: + self.assign(self.float_param, self.get(self.float_preset) * 2) class TestGeneratorMultiParameter(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(GeneratorMultiParameter(5.0, 10.0)) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(GeneratorMultiParameter(5.0, 10.0)) class GeneratorMultiParameter(GeneratorBlock): - def __init__(self, float_preset1: FloatLike, float_preset2: FloatLike) -> None: - super().__init__() - self.float_param1 = self.Parameter(FloatExpr()) - self.float_param2 = self.Parameter(FloatExpr()) - self.float_preset1 = self.ArgParameter(float_preset1) - self.float_preset2 = self.ArgParameter(float_preset2) - self.generator_param(self.float_preset1, self.float_preset2) + def __init__(self, float_preset1: FloatLike, float_preset2: FloatLike) -> None: + super().__init__() + self.float_param1 = self.Parameter(FloatExpr()) + self.float_param2 = self.Parameter(FloatExpr()) + self.float_preset1 = self.ArgParameter(float_preset1) + self.float_preset2 = self.ArgParameter(float_preset2) + self.generator_param(self.float_preset1, self.float_preset2) - @override - def generate(self) -> None: - self.assign1 = self.assign(self.float_param1, self.get(self.float_preset1) * 3) - self.assign2 = self.assign(self.float_param2, self.get(self.float_preset2) + 7) + @override + def generate(self) -> None: + self.assign1 = self.assign(self.float_param1, self.get(self.float_preset1) * 3) + self.assign2 = self.assign(self.float_param2, self.get(self.float_preset2) + 7) class TestGenerator(unittest.TestCase): - def test_generator_assign(self) -> None: - compiled = ScalaCompiler.compile(TestGeneratorAssign) + def test_generator_assign(self) -> None: + compiled = ScalaCompiler.compile(TestGeneratorAssign) - self.assertEqual(compiled.get_value(['block', 'float_param']), 2.0) + self.assertEqual(compiled.get_value(["block", "float_param"]), 2.0) - def test_generator_dependency(self) -> None: - compiled = ScalaCompiler.compile(TestGeneratorDependency) + def test_generator_dependency(self) -> None: + compiled = ScalaCompiler.compile(TestGeneratorDependency) - self.assertEqual(compiled.get_value(['block', 'float_param']), 6.0) + self.assertEqual(compiled.get_value(["block", "float_param"]), 6.0) - def test_generator_multi_dependency(self) -> None: - compiled = ScalaCompiler.compile(TestGeneratorMultiParameter) + def test_generator_multi_dependency(self) -> None: + compiled = ScalaCompiler.compile(TestGeneratorMultiParameter) - self.assertEqual(compiled.get_value(['block', 'float_param1']), 15.0) - self.assertEqual(compiled.get_value(['block', 'float_param2']), 17.0) + self.assertEqual(compiled.get_value(["block", "float_param1"]), 15.0) + self.assertEqual(compiled.get_value(["block", "float_param2"]), 17.0) class TestLink(Link): - def __init__(self) -> None: - super().__init__() - self.source = self.Port(TestPortSource(), optional=True) - self.sinks = self.Port(Vector(TestPortSink()), optional=True) - self.source_float = self.Parameter(FloatExpr(self.source.float_param)) - self.sinks_range = self.Parameter(RangeExpr(self.sinks.intersection(lambda x: x.range_param))) + def __init__(self) -> None: + super().__init__() + self.source = self.Port(TestPortSource(), optional=True) + self.sinks = self.Port(Vector(TestPortSink()), optional=True) + self.source_float = self.Parameter(FloatExpr(self.source.float_param)) + self.sinks_range = self.Parameter(RangeExpr(self.sinks.intersection(lambda x: x.range_param))) class TestPortSource(Port[TestLink]): - link_type = TestLink + link_type = TestLink - def __init__(self, float_value: FloatLike = FloatExpr()) -> None: - super().__init__() - self.float_param = self.Parameter(FloatExpr(float_value)) + def __init__(self, float_value: FloatLike = FloatExpr()) -> None: + super().__init__() + self.float_param = self.Parameter(FloatExpr(float_value)) class TestPortSink(Port[TestLink]): - link_type = TestLink + link_type = TestLink - def __init__(self, range_value: RangeLike = RangeExpr()) -> None: - super().__init__() - self.range_param = self.Parameter(RangeExpr(range_value)) + def __init__(self, range_value: RangeLike = RangeExpr()) -> None: + super().__init__() + self.range_param = self.Parameter(RangeExpr(range_value)) class TestBlockSource(Block): - def __init__(self, float_value: FloatLike) -> None: - super().__init__() - self.port = self.Port(TestPortSource(float_value)) + def __init__(self, float_value: FloatLike) -> None: + super().__init__() + self.port = self.Port(TestPortSource(float_value)) class TestBlockSink(Block): - def __init__(self, range_value: RangeLike) -> None: - super().__init__() - self.port = self.Port(TestPortSink(range_value)) + def __init__(self, range_value: RangeLike) -> None: + super().__init__() + self.port = self.Port(TestPortSink(range_value)) class GeneratorIsConnected(GeneratorBlock): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(TestPortSource(2.0), optional=True) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(TestPortSource(2.0), optional=True) - self.generator_param(self.port.is_connected()) - self.connected = self.Parameter(BoolExpr()) + self.generator_param(self.port.is_connected()) + self.connected = self.Parameter(BoolExpr()) - @override - def generate(self) -> None: - if self.get(self.port.is_connected()): - self.assign(self.connected, True) - else: - self.assign(self.connected, False) + @override + def generate(self) -> None: + if self.get(self.port.is_connected()): + self.assign(self.connected, True) + else: + self.assign(self.connected, False) class TestGeneratorConnectedTop(Block): - def __init__(self) -> None: - super().__init__() - self.generator = self.Block(GeneratorIsConnected()) - self.sink = self.Block(TestBlockSink((0.5, 2.5))) - self.link = self.connect(self.generator.port, self.sink.port) + def __init__(self) -> None: + super().__init__() + self.generator = self.Block(GeneratorIsConnected()) + self.sink = self.Block(TestBlockSink((0.5, 2.5))) + self.link = self.connect(self.generator.port, self.sink.port) class TestGeneratorNotConnectedTop(Block): - def __init__(self) -> None: - super().__init__() - self.generator = self.Block(GeneratorIsConnected()) + def __init__(self) -> None: + super().__init__() + self.generator = self.Block(GeneratorIsConnected()) class GeneratorInnerConnect(GeneratorBlock): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(TestPortSource(), optional=True) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(TestPortSource(), optional=True) - @override - def generate(self) -> None: - self.inner = self.Block(TestBlockSource(4.5)) - self.connect(self.inner.port, self.port) + @override + def generate(self) -> None: + self.inner = self.Block(TestBlockSource(4.5)) + self.connect(self.inner.port, self.port) class TestGeneratorInnerConnectTop(Block): - def __init__(self) -> None: - super().__init__() - self.generator = self.Block(GeneratorInnerConnect()) - self.sink = self.Block(TestBlockSink((1.5, 3.5))) - self.link = self.connect(self.generator.port, self.sink.port) + def __init__(self) -> None: + super().__init__() + self.generator = self.Block(GeneratorInnerConnect()) + self.sink = self.Block(TestBlockSink((1.5, 3.5))) + self.link = self.connect(self.generator.port, self.sink.port) class TestGeneratorConnect(unittest.TestCase): - def test_generator_connected(self) -> None: - compiled = ScalaCompiler.compile(TestGeneratorConnectedTop) + def test_generator_connected(self) -> None: + compiled = ScalaCompiler.compile(TestGeneratorConnectedTop) - self.assertEqual(compiled.get_value(['generator', 'connected']), True) - self.assertEqual(compiled.get_value(['link', 'source_float']), 2.0) - self.assertEqual(compiled.get_value(['link', 'sinks_range']), Range(0.5, 2.5)) + self.assertEqual(compiled.get_value(["generator", "connected"]), True) + self.assertEqual(compiled.get_value(["link", "source_float"]), 2.0) + self.assertEqual(compiled.get_value(["link", "sinks_range"]), Range(0.5, 2.5)) - def test_generator_not_connected(self) -> None: - compiled = ScalaCompiler.compile(TestGeneratorNotConnectedTop) + def test_generator_not_connected(self) -> None: + compiled = ScalaCompiler.compile(TestGeneratorNotConnectedTop) - self.assertEqual(compiled.get_value(['generator', 'connected']), False) + self.assertEqual(compiled.get_value(["generator", "connected"]), False) - def test_generator_inner_connect(self) -> None: - compiled = ScalaCompiler.compile(TestGeneratorInnerConnectTop) + def test_generator_inner_connect(self) -> None: + compiled = ScalaCompiler.compile(TestGeneratorInnerConnectTop) - self.assertEqual(compiled.get_value(['link', 'source_float']), 4.5) - self.assertEqual(compiled.get_value(['link', 'sinks_range']), Range(1.5, 3.5)) + self.assertEqual(compiled.get_value(["link", "source_float"]), 4.5) + self.assertEqual(compiled.get_value(["link", "sinks_range"]), Range(1.5, 3.5)) class TestGeneratorException(BaseException): - pass + pass class TestGeneratorFailure(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(GeneratorFailure()) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(GeneratorFailure()) class GeneratorFailure(GeneratorBlock): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - @override - def generate(self) -> None: - def helperfn() -> None: - raise TestGeneratorException("test text") - helperfn() + @override + def generate(self) -> None: + def helperfn() -> None: + raise TestGeneratorException("test text") + + helperfn() class GeneratorFailureTestCase(unittest.TestCase): - def test_metadata(self) -> None: - with self.assertRaises(CompilerCheckError), \ - open(devnull, 'w') as fnull, redirect_stderr(fnull): # suppress generator error - self.compiled = ScalaCompiler.compile(TestGeneratorFailure) + def test_metadata(self) -> None: + with ( + self.assertRaises(CompilerCheckError), + open(devnull, "w") as fnull, + redirect_stderr(fnull), + ): # suppress generator error + self.compiled = ScalaCompiler.compile(TestGeneratorFailure) diff --git a/edg/core/test_generator_error.py b/edg/core/test_generator_error.py index 2cafe196c..da5f5d4c3 100644 --- a/edg/core/test_generator_error.py +++ b/edg/core/test_generator_error.py @@ -7,26 +7,25 @@ class BadGeneratorTestCase(unittest.TestCase): - # These are internal classes to avoid this error case being auto-discovered in a library + # These are internal classes to avoid this error case being auto-discovered in a library - class InvalidMissingGeneratorBlock(GeneratorBlock): - """This doesn't implement generator()""" + class InvalidMissingGeneratorBlock(GeneratorBlock): + """This doesn't implement generator()""" - def test_missing_generator(self) -> None: - with self.assertRaises(BlockDefinitionError): - self.InvalidMissingGeneratorBlock()._elaborated_def_to_proto() + def test_missing_generator(self) -> None: + with self.assertRaises(BlockDefinitionError): + self.InvalidMissingGeneratorBlock()._elaborated_def_to_proto() + class InvalidNonArgGeneratorBlock(GeneratorBlock): + def __init__(self) -> None: + super().__init__() + self.param = self.Parameter(FloatExpr()) + self.generator_param(self.param) - class InvalidNonArgGeneratorBlock(GeneratorBlock): - def __init__(self) -> None: - super().__init__() - self.param = self.Parameter(FloatExpr()) - self.generator_param(self.param) + @override + def generate(self) -> None: + super().generate() - @override - def generate(self) -> None: - super().generate() - - def test_non_arg_generator(self) -> None: - with self.assertRaises(BlockDefinitionError): - self.InvalidNonArgGeneratorBlock()._elaborated_def_to_proto() + def test_non_arg_generator(self) -> None: + with self.assertRaises(BlockDefinitionError): + self.InvalidNonArgGeneratorBlock()._elaborated_def_to_proto() diff --git a/edg/core/test_generator_portvector.py b/edg/core/test_generator_portvector.py index 7de880c07..c8bfca0b2 100644 --- a/edg/core/test_generator_portvector.py +++ b/edg/core/test_generator_portvector.py @@ -9,136 +9,135 @@ class GeneratorInnerBlock(GeneratorBlock): - def __init__(self) -> None: - super().__init__() - self.ports = self.Port(Vector(TestPortSink())) - self.generator_param(self.ports.requested()) + def __init__(self) -> None: + super().__init__() + self.ports = self.Port(Vector(TestPortSink())) + self.generator_param(self.ports.requested()) - @override - def generate(self) -> None: - assert self.get(self.ports.requested()) == ['0', 'named', '1'] - self.ports.append_elt(TestPortSink((-1, 1))) - self.ports.append_elt(TestPortSink((-5, 5)), 'named') - self.ports.append_elt(TestPortSink((-2, 2))) + @override + def generate(self) -> None: + assert self.get(self.ports.requested()) == ["0", "named", "1"] + self.ports.append_elt(TestPortSink((-1, 1))) + self.ports.append_elt(TestPortSink((-5, 5)), "named") + self.ports.append_elt(TestPortSink((-2, 2))) class TestGeneratorElements(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(GeneratorInnerBlock()) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(GeneratorInnerBlock()) - self.source0 = self.Block(TestBlockSource(1.0)) - self.source1 = self.Block(TestBlockSource(1.0)) - self.source2 = self.Block(TestBlockSource(1.0)) - self.connect(self.source0.port, self.block.ports.request()) - self.connect(self.source1.port, self.block.ports.request('named')) - self.connect(self.source2.port, self.block.ports.request()) + self.source0 = self.Block(TestBlockSource(1.0)) + self.source1 = self.Block(TestBlockSource(1.0)) + self.source2 = self.Block(TestBlockSource(1.0)) + self.connect(self.source0.port, self.block.ports.request()) + self.connect(self.source1.port, self.block.ports.request("named")) + self.connect(self.source2.port, self.block.ports.request()) class TestGeneratorPortVector(unittest.TestCase): - def test_generator(self) -> None: - ScalaCompiler.compile(TestGeneratorElements) + def test_generator(self) -> None: + ScalaCompiler.compile(TestGeneratorElements) - def test_initializer(self) -> None: - compiled = ScalaCompiler.compile(TestGeneratorElements) - pb = compiled.contents.blocks[0].value.hierarchy - self.assertEqual(pb.constraints[1].name, "(init)ports.0.range_param") - self.assertEqual(pb.constraints[1].value, edgir.AssignLit(['ports', '0', 'range_param'], Range(-1, 1))) - self.assertEqual(pb.constraints[2].name, "(init)ports.named.range_param") - self.assertEqual(pb.constraints[2].value, edgir.AssignLit(['ports', 'named', 'range_param'], Range(-5, 5))) - self.assertEqual(pb.constraints[3].name, "(init)ports.1.range_param") - self.assertEqual(pb.constraints[3].value, edgir.AssignLit(['ports', '1', 'range_param'], Range(-2, 2))) + def test_initializer(self) -> None: + compiled = ScalaCompiler.compile(TestGeneratorElements) + pb = compiled.contents.blocks[0].value.hierarchy + self.assertEqual(pb.constraints[1].name, "(init)ports.0.range_param") + self.assertEqual(pb.constraints[1].value, edgir.AssignLit(["ports", "0", "range_param"], Range(-1, 1))) + self.assertEqual(pb.constraints[2].name, "(init)ports.named.range_param") + self.assertEqual(pb.constraints[2].value, edgir.AssignLit(["ports", "named", "range_param"], Range(-5, 5))) + self.assertEqual(pb.constraints[3].name, "(init)ports.1.range_param") + self.assertEqual(pb.constraints[3].value, edgir.AssignLit(["ports", "1", "range_param"], Range(-2, 2))) class InnerBlockInvalid(Block): - def __init__(self) -> None: - super().__init__() - self.ports = self.Port(Vector(TestPortSink())) - self.ports.append_elt(TestPortSink(), 'haha') + def __init__(self) -> None: + super().__init__() + self.ports = self.Port(Vector(TestPortSink())) + self.ports.append_elt(TestPortSink(), "haha") class TestElementsInvalid(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(InnerBlockInvalid()) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(InnerBlockInvalid()) - self.source0 = self.Block(TestBlockSource(1.0)) - self.connect(self.source0.port, self.block.ports.request('nope')) + self.source0 = self.Block(TestBlockSource(1.0)) + self.connect(self.source0.port, self.block.ports.request("nope")) class TestPortVectorInvalid(unittest.TestCase): - def test_generator_error(self) -> None: - with self.assertRaises(CompilerCheckError): - ScalaCompiler.compile(TestElementsInvalid) + def test_generator_error(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(TestElementsInvalid) class GeneratorWrapperBlock(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(GeneratorInnerBlock()) - self.ports = self.Export(self.block.ports) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(GeneratorInnerBlock()) + self.ports = self.Export(self.block.ports) class GeneratorWrapperTest(Block): # same as TestGeneratorElements, but creating a GeneratorInnerBlock - def __init__(self) -> None: - super().__init__() - self.block = self.Block(GeneratorWrapperBlock()) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(GeneratorWrapperBlock()) - self.source0 = self.Block(TestBlockSource(1.0)) - self.source1 = self.Block(TestBlockSource(1.0)) - self.source2 = self.Block(TestBlockSource(1.0)) - self.connect(self.source0.port, self.block.ports.request()) - self.connect(self.source1.port, self.block.ports.request('named')) - self.connect(self.source2.port, self.block.ports.request()) + self.source0 = self.Block(TestBlockSource(1.0)) + self.source1 = self.Block(TestBlockSource(1.0)) + self.source2 = self.Block(TestBlockSource(1.0)) + self.connect(self.source0.port, self.block.ports.request()) + self.connect(self.source1.port, self.block.ports.request("named")) + self.connect(self.source2.port, self.block.ports.request()) class TestGeneratorWrapper(unittest.TestCase): - def test_generator(self) -> None: - ScalaCompiler.compile(GeneratorWrapperTest) + def test_generator(self) -> None: + ScalaCompiler.compile(GeneratorWrapperTest) - def test_exported_ports(self) -> None: - compiled = ScalaCompiler.compile(GeneratorWrapperTest) - pb = edgir.pair_get(compiled.contents.blocks, 'block').hierarchy + def test_exported_ports(self) -> None: + compiled = ScalaCompiler.compile(GeneratorWrapperTest) + pb = edgir.pair_get(compiled.contents.blocks, "block").hierarchy - # check the inner block too - inner_block = edgir.pair_get(pb.blocks, 'block').hierarchy - pb_ports = edgir.pair_get(inner_block.ports, 'ports').array.ports.ports - self.assertEqual(pb_ports[0].name, '0') - self.assertEqual(pb_ports[1].name, 'named') - self.assertEqual(pb_ports[2].name, '1') + # check the inner block too + inner_block = edgir.pair_get(pb.blocks, "block").hierarchy + pb_ports = edgir.pair_get(inner_block.ports, "ports").array.ports.ports + self.assertEqual(pb_ports[0].name, "0") + self.assertEqual(pb_ports[1].name, "named") + self.assertEqual(pb_ports[2].name, "1") - pb_ports = edgir.pair_get(pb.ports, 'ports').array.ports.ports - self.assertEqual(pb_ports[0].name, '0') - self.assertEqual(pb_ports[1].name, 'named') - self.assertEqual(pb_ports[2].name, '1') + pb_ports = edgir.pair_get(pb.ports, "ports").array.ports.ports + self.assertEqual(pb_ports[0].name, "0") + self.assertEqual(pb_ports[1].name, "named") + self.assertEqual(pb_ports[2].name, "1") class GeneratorArrayParam(GeneratorBlock): - def __init__(self, param: ArrayRangeLike) -> None: - super().__init__() - self.ports = self.Port(Vector(TestPortSink())) - self.param = self.ArgParameter(param) - self.generator_param(self.param) + def __init__(self, param: ArrayRangeLike) -> None: + super().__init__() + self.ports = self.Port(Vector(TestPortSink())) + self.param = self.ArgParameter(param) + self.generator_param(self.param) - @override - def generate(self) -> None: - for elt in self.get(self.param): - created_port = self.ports.append_elt(TestPortSink(elt)) # any port - self.require(created_port.link().sinks_range == Range(-2, 1)) + @override + def generate(self) -> None: + for elt in self.get(self.param): + created_port = self.ports.append_elt(TestPortSink(elt)) # any port + self.require(created_port.link().sinks_range == Range(-2, 1)) class GeneratorArrayParamTop(Block): - def __init__(self) -> None: - super().__init__() - self.block = self.Block(GeneratorArrayParam([ - (-3, 1), (-5, 5), (-2, 2) - ])) + def __init__(self) -> None: + super().__init__() + self.block = self.Block(GeneratorArrayParam([(-3, 1), (-5, 5), (-2, 2)])) - self.source = self.Block(TestBlockSource(1.0)) - self.connect(self.source.port, - self.block.ports.request(), self.block.ports.request(), self.block.ports.request()) + self.source = self.Block(TestBlockSource(1.0)) + self.connect( + self.source.port, self.block.ports.request(), self.block.ports.request(), self.block.ports.request() + ) class TestGeneratorArrayParam(unittest.TestCase): - def test_generator(self) -> None: - ScalaCompiler.compile(GeneratorArrayParamTop) + def test_generator(self) -> None: + ScalaCompiler.compile(GeneratorArrayParamTop) diff --git a/edg/core/test_hierarchy_block.py b/edg/core/test_hierarchy_block.py index fdcd26df5..d7a388a68 100644 --- a/edg/core/test_hierarchy_block.py +++ b/edg/core/test_hierarchy_block.py @@ -8,300 +8,300 @@ class TopHierarchyBlock(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestBlockSource()) - self.sink1 = self.Block(TestBlockSink()) - self.sink2 = self.Block(TestBlockSink()) - self.test_net = self.connect(self.source.source, self.sink1.sink, self.sink2.sink) + self.source = self.Block(TestBlockSource()) + self.sink1 = self.Block(TestBlockSink()) + self.sink2 = self.Block(TestBlockSink()) + self.test_net = self.connect(self.source.source, self.sink1.sink, self.sink2.sink) class TopHierarchyBlockProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TopHierarchyBlock()._elaborated_def_to_proto() - - def test_subblock_def(self) -> None: - self.assertEqual(self.pb.blocks[0].name, 'source') - self.assertEqual(self.pb.blocks[0].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSource") - self.assertFalse(self.pb.blocks[0].value.lib_elem.mixins) - self.assertEqual(self.pb.blocks[1].name, 'sink1') - self.assertEqual(self.pb.blocks[1].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") - self.assertFalse(self.pb.blocks[1].value.lib_elem.mixins) - self.assertEqual(self.pb.blocks[2].name, 'sink2') - self.assertEqual(self.pb.blocks[2].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") - self.assertFalse(self.pb.blocks[2].value.lib_elem.mixins) - - def test_link_inference(self) -> None: - self.assertEqual(len(self.pb.links), 1) - self.assertEqual(self.pb.links[0].name, 'test_net') - self.assertEqual(self.pb.links[0].value.lib_elem.target.name, "edg.core.test_common.TestLink") - - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 3) # TODO: maybe filter by connection types in future for robustness - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink1' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink2' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = TopHierarchyBlock()._elaborated_def_to_proto() + + def test_subblock_def(self) -> None: + self.assertEqual(self.pb.blocks[0].name, "source") + self.assertEqual(self.pb.blocks[0].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSource") + self.assertFalse(self.pb.blocks[0].value.lib_elem.mixins) + self.assertEqual(self.pb.blocks[1].name, "sink1") + self.assertEqual(self.pb.blocks[1].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") + self.assertFalse(self.pb.blocks[1].value.lib_elem.mixins) + self.assertEqual(self.pb.blocks[2].name, "sink2") + self.assertEqual(self.pb.blocks[2].value.lib_elem.base.target.name, "edg.core.test_common.TestBlockSink") + self.assertFalse(self.pb.blocks[2].value.lib_elem.mixins) + + def test_link_inference(self) -> None: + self.assertEqual(len(self.pb.links), 1) + self.assertEqual(self.pb.links[0].name, "test_net") + self.assertEqual(self.pb.links[0].value.lib_elem.target.name, "edg.core.test_common.TestLink") + + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 3) # TODO: maybe filter by connection types in future for robustness + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink1" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink2" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) class MultiConnectBlock(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestBlockSource()) - self.sink1 = self.Block(TestBlockSink()) - self.sink2 = self.Block(TestBlockSink()) - self.sink3 = self.Block(TestBlockSink()) + self.source = self.Block(TestBlockSource()) + self.sink1 = self.Block(TestBlockSink()) + self.sink2 = self.Block(TestBlockSink()) + self.sink3 = self.Block(TestBlockSink()) - self.test_net = self.connect(self.source.source, self.sink1.sink) - self.connect(self.source.source, self.sink2.sink) # not named, to not conflict with the first - self.connect(self.test_net, self.sink3.sink) + self.test_net = self.connect(self.source.source, self.sink1.sink) + self.connect(self.source.source, self.sink2.sink) # not named, to not conflict with the first + self.connect(self.test_net, self.sink3.sink) class MultiConnectBlockProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = MultiConnectBlock()._elaborated_def_to_proto() - - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 4) - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink1' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink2' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink3' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = MultiConnectBlock()._elaborated_def_to_proto() + + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 4) + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink1" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink2" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink3" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) class ConnectJoinBlock(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestBlockSource()) - self.sink1 = self.Block(TestBlockSink()) - self.sink2 = self.Block(TestBlockSink()) - self.sink3 = self.Block(TestBlockSink()) - self.sink4 = self.Block(TestBlockSink()) + self.source = self.Block(TestBlockSource()) + self.sink1 = self.Block(TestBlockSink()) + self.sink2 = self.Block(TestBlockSink()) + self.sink3 = self.Block(TestBlockSink()) + self.sink4 = self.Block(TestBlockSink()) - self.test_net = self.connect(self.source.source, self.sink1.sink) # authoritative name - self.connect(self.sink2.sink, self.sink3.sink) # not named, to not conflict with the first - self.connect(self.source.source, self.sink3.sink) - self.connect(self.sink3.sink, self.sink4.sink) # connect to the overridden net + self.test_net = self.connect(self.source.source, self.sink1.sink) # authoritative name + self.connect(self.sink2.sink, self.sink3.sink) # not named, to not conflict with the first + self.connect(self.source.source, self.sink3.sink) + self.connect(self.sink3.sink, self.sink4.sink) # connect to the overridden net class ConnectJoinBlockProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = ConnectJoinBlock()._elaborated_def_to_proto() - - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 5) - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink1' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink2' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink3' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink4' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = ConnectJoinBlock()._elaborated_def_to_proto() + + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 5) + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink1" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink2" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink3" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink4" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) class ExportPortHierarchyBlock(Block): - def __init__(self) -> None: - super().__init__() - self.sink = self.Block(TestBlockSink()) - self.exported = self.Export(self.sink.sink, optional=True) # avoid required constraint + def __init__(self) -> None: + super().__init__() + self.sink = self.Block(TestBlockSink()) + self.exported = self.Export(self.sink.sink, optional=True) # avoid required constraint class ExportPortHierarchyBlockTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = ExportPortHierarchyBlock()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = ExportPortHierarchyBlock()._elaborated_def_to_proto() - def test_exported_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 1) - self.assertEqual(self.pb.ports[0].name, 'exported') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") + def test_exported_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 1) + self.assertEqual(self.pb.ports[0].name, "exported") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 1) - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 1) + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'exported' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'sink' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "exported" + expected_conn.exported.internal_block_port.ref.steps.add().name = "sink" + expected_conn.exported.internal_block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) class IndirectExportPortHierarchyBlock(Block): - def __init__(self) -> None: - super().__init__() - self.exported = self.Port(TestPortSink(), optional=True) # avoid required constraint + def __init__(self) -> None: + super().__init__() + self.exported = self.Port(TestPortSink(), optional=True) # avoid required constraint - @override - def contents(self) -> None: - super().contents() - self.sink = self.Block(TestBlockSink()) - self.test_net = self.connect(self.exported, self.sink.sink) + @override + def contents(self) -> None: + super().contents() + self.sink = self.Block(TestBlockSink()) + self.test_net = self.connect(self.exported, self.sink.sink) class IndirectExportPortHierarchyBlockTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = IndirectExportPortHierarchyBlock()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = IndirectExportPortHierarchyBlock()._elaborated_def_to_proto() - def test_exported_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 1) - self.assertEqual(self.pb.ports[0].name, 'exported') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") + def test_exported_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 1) + self.assertEqual(self.pb.ports[0].name, "exported") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 1) - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 1) + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'exported' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'sink' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "exported" + expected_conn.exported.internal_block_port.ref.steps.add().name = "sink" + expected_conn.exported.internal_block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) class PortBridgeHierarchyBlock(Block): - def __init__(self) -> None: - super().__init__() - self.source_port = self.Port(TestPortSink(), optional=True) + def __init__(self) -> None: + super().__init__() + self.source_port = self.Port(TestPortSink(), optional=True) - @override - def contents(self) -> None: - super().contents() - self.sink1 = self.Block(TestBlockSink()) - self.sink2 = self.Block(TestBlockSink()) - self.test_net = self.connect(self.source_port, self.sink1.sink, self.sink2.sink) + @override + def contents(self) -> None: + super().contents() + self.sink1 = self.Block(TestBlockSink()) + self.sink2 = self.Block(TestBlockSink()) + self.test_net = self.connect(self.source_port, self.sink1.sink, self.sink2.sink) class PortBridgeHierarchyBlockTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = PortBridgeHierarchyBlock()._elaborated_def_to_proto() - - def test_exported_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 1) - self.assertEqual(self.pb.ports[0].name, 'source_port') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") - - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 4) - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = '(bridge)source_port' - expected_conn.connected.block_port.ref.steps.add().name = 'inner_link' - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'source_port' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(bridge)source_port' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'outer_port' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'sink1' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'sink2' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = PortBridgeHierarchyBlock()._elaborated_def_to_proto() + + def test_exported_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 1) + self.assertEqual(self.pb.ports[0].name, "source_port") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") + + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 4) + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "(bridge)source_port" + expected_conn.connected.block_port.ref.steps.add().name = "inner_link" + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "source_port" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(bridge)source_port" + expected_conn.exported.internal_block_port.ref.steps.add().name = "outer_port" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "sink1" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "sink2" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) diff --git a/edg/core/test_implicit_connect.py b/edg/core/test_implicit_connect.py index 749722a12..feef0791a 100644 --- a/edg/core/test_implicit_connect.py +++ b/edg/core/test_implicit_connect.py @@ -9,104 +9,101 @@ class ImplicitConnectBlock(Block): - """Block with implicit scope containing some blocks""" - @override - def contents(self) -> None: - super().contents() - self.block_source = self.Block(TestBlockSource()) - self.implicit_net = self.connect(self.block_source.source) # TODO better syntax for naming + """Block with implicit scope containing some blocks""" - with self.implicit_connect( - ImplicitConnect(self.block_source.source, [ImplicitSink]) - ) as imp: - self.imp_block_sink = imp.Block(TestBlockImplicitSink()) # should have implicit connect - self.int_block_sink = imp.Block(TestBlockSink()) # should not have implicit connect (untagged) - self.int_block_source = imp.Block(TestBlockSource()) # should not have implicit connect + @override + def contents(self) -> None: + super().contents() + self.block_source = self.Block(TestBlockSource()) + self.implicit_net = self.connect(self.block_source.source) # TODO better syntax for naming + + with self.implicit_connect(ImplicitConnect(self.block_source.source, [ImplicitSink])) as imp: + self.imp_block_sink = imp.Block(TestBlockImplicitSink()) # should have implicit connect + self.int_block_sink = imp.Block(TestBlockSink()) # should not have implicit connect (untagged) + self.int_block_source = imp.Block(TestBlockSource()) # should not have implicit connect class ImplicitConnectTestCase(unittest.TestCase): - def test_connectivity(self) -> None: - pb = ImplicitConnectBlock()._elaborated_def_to_proto() + def test_connectivity(self) -> None: + pb = ImplicitConnectBlock()._elaborated_def_to_proto() - self.assertEqual(len(pb.constraints), 2) - constraints = list(map(lambda pair: pair.value, pb.constraints)) + self.assertEqual(len(pb.constraints), 2) + constraints = list(map(lambda pair: pair.value, pb.constraints)) - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'implicit_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'imp_block_sink' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "implicit_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "imp_block_sink" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'implicit_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'block_source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "implicit_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "block_source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) class ExportedImplicitConnectBlock(Block): - """Block with implicit external port that is bridged""" - def __init__(self) -> None: - super().__init__() - self.sink_in = self.Port(TestPortSink()) - - @override - def contents(self) -> None: - super().contents() - self.implicit_net = self.connect(self.sink_in) - with self.implicit_connect( - ImplicitConnect(self.sink_in, [ImplicitSink]) - ) as imp: - # Note, we have two so this generates as a link instead of export - self.block_sink_0 = imp.Block(TestBlockImplicitSink()) # should have implicit connect - self.block_sink_1 = imp.Block(TestBlockImplicitSink()) # should have implicit connect - self.block_sink_2 = imp.Block(TestBlockSink()) # should not have implicit connect (untagged) - self.block_source = imp.Block(TestBlockSource()) # should not have implicit connect + """Block with implicit external port that is bridged""" + + def __init__(self) -> None: + super().__init__() + self.sink_in = self.Port(TestPortSink()) + + @override + def contents(self) -> None: + super().contents() + self.implicit_net = self.connect(self.sink_in) + with self.implicit_connect(ImplicitConnect(self.sink_in, [ImplicitSink])) as imp: + # Note, we have two so this generates as a link instead of export + self.block_sink_0 = imp.Block(TestBlockImplicitSink()) # should have implicit connect + self.block_sink_1 = imp.Block(TestBlockImplicitSink()) # should have implicit connect + self.block_sink_2 = imp.Block(TestBlockSink()) # should not have implicit connect (untagged) + self.block_source = imp.Block(TestBlockSource()) # should not have implicit connect class ExportedImplicitConnectTestCase(unittest.TestCase): - def test_connectivity(self) -> None: - pb = ExportedImplicitConnectBlock()._elaborated_def_to_proto() + def test_connectivity(self) -> None: + pb = ExportedImplicitConnectBlock()._elaborated_def_to_proto() - self.assertEqual(len(pb.constraints), 5) # including source export, connect, port required - constraints = list(map(lambda pair: pair.value, pb.constraints)) + self.assertEqual(len(pb.constraints), 5) # including source export, connect, port required + constraints = list(map(lambda pair: pair.value, pb.constraints)) - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'implicit_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'block_sink_0' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "implicit_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "block_sink_0" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'implicit_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'block_sink_1' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "implicit_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "block_sink_1" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) class ImplicitConnectOutsideScopeErrorBlock(Block): - """Block with implicit scope containing some blocks""" - @override - def contents(self) -> None: - super().contents() - self.block_source = self.Block(TestBlockSource()) + """Block with implicit scope containing some blocks""" + + @override + def contents(self) -> None: + super().contents() + self.block_source = self.Block(TestBlockSource()) - with self.implicit_connect( - ImplicitConnect(self.block_source.source, [ImplicitSink]) - ) as imp: - pass + with self.implicit_connect(ImplicitConnect(self.block_source.source, [ImplicitSink])) as imp: + pass - self.imp_block_sink = imp.Block(TestBlockImplicitSink()) # should have implicit connect + self.imp_block_sink = imp.Block(TestBlockImplicitSink()) # should have implicit connect class ImplicitConnectOutsideScopeErrorTestCase(unittest.TestCase): - def test_error(self) -> None: - with self.assertRaises(EdgContextError) as context: - ImplicitConnectOutsideScopeErrorBlock()._elaborated_def_to_proto() + def test_error(self) -> None: + with self.assertRaises(EdgContextError) as context: + ImplicitConnectOutsideScopeErrorBlock()._elaborated_def_to_proto() diff --git a/edg/core/test_initializer.py b/edg/core/test_initializer.py index 74ff7000b..8d9f2cac6 100644 --- a/edg/core/test_initializer.py +++ b/edg/core/test_initializer.py @@ -8,94 +8,94 @@ class TestSingleInitializerBlock(Block): - def __init__(self) -> None: - super().__init__() - self.bundle_port = self.Port(TestBundle(42, 1, -1), optional=True) + def __init__(self) -> None: + super().__init__() + self.bundle_port = self.Port(TestBundle(42, 1, -1), optional=True) class TestInternalBlock(Block): - def __init__(self, inner_param: FloatLike = 3.0, bundle_param: FloatLike = FloatExpr()) -> None: - super().__init__() - self.inner_bundle = self.Port(TestBundle(bundle_param, 0, 24), optional=True) - self.inner_param = inner_param + def __init__(self, inner_param: FloatLike = 3.0, bundle_param: FloatLike = FloatExpr()) -> None: + super().__init__() + self.inner_bundle = self.Port(TestBundle(bundle_param, 0, 24), optional=True) + self.inner_param = inner_param class TestNestedBlock(Block): - def __init__(self) -> None: - super().__init__() - self.outer_bundle = self.Port(TestBundle(21, 1, -1), optional=True) + def __init__(self) -> None: + super().__init__() + self.outer_bundle = self.Port(TestBundle(21, 1, -1), optional=True) - @override - def contents(self) -> None: - super().contents() - self.inner = self.Block(TestInternalBlock(62, 31)) + @override + def contents(self) -> None: + super().contents() + self.inner = self.Block(TestInternalBlock(62, 31)) class TestDefaultBlock(Block): - @override - def contents(self) -> None: - super().contents() - self.inner = self.Block(TestInternalBlock()) + @override + def contents(self) -> None: + super().contents() + self.inner = self.Block(TestInternalBlock()) class TestMultipleInstantiationBlock(Block): - @override - def contents(self) -> None: - super().contents() - model = TestInternalBlock() - self.inner1 = self.Block(model) - self.inner2 = self.Block(model) + @override + def contents(self) -> None: + super().contents() + model = TestInternalBlock() + self.inner1 = self.Block(model) + self.inner2 = self.Block(model) class InitializerTestCase(unittest.TestCase): - def test_initializer(self) -> None: - pb = TestSingleInitializerBlock()._elaborated_def_to_proto() - - self.assertEqual(len(pb.constraints), 3) - self.assertEqual(pb.constraints[0].name, "(init)bundle_port.float_param") - self.assertEqual(pb.constraints[0].value, edgir.AssignLit(['bundle_port', 'float_param'], 42.0)) - self.assertEqual(pb.constraints[1].name, "(init)bundle_port.a.float_param") - self.assertEqual(pb.constraints[1].value, edgir.AssignLit(['bundle_port', 'a', 'float_param'], 1.0)) - self.assertEqual(pb.constraints[2].name, "(init)bundle_port.b.float_param") - self.assertEqual(pb.constraints[2].value, edgir.AssignLit(['bundle_port', 'b', 'float_param'], -1.0)) - - def test_nested_initializer(self) -> None: - pb = TestNestedBlock()._elaborated_def_to_proto() - - self.assertEqual(len(pb.constraints), 5) - self.assertEqual(pb.constraints[0].name, "(init)outer_bundle.float_param") - self.assertEqual(pb.constraints[0].value, edgir.AssignLit(['outer_bundle', 'float_param'], 21.0)) - self.assertEqual(pb.constraints[1].name, "(init)outer_bundle.a.float_param") - self.assertEqual(pb.constraints[1].value, edgir.AssignLit(['outer_bundle', 'a', 'float_param'], 1.0)) - self.assertEqual(pb.constraints[2].name, "(init)outer_bundle.b.float_param") - self.assertEqual(pb.constraints[2].value, edgir.AssignLit(['outer_bundle', 'b', 'float_param'], -1.0)) - self.assertEqual(pb.constraints[3].name, "(init)inner.inner_param") - self.assertEqual(pb.constraints[3].value, edgir.AssignLit(['inner', 'inner_param'], 62.0)) - self.assertEqual(pb.constraints[4].name, "(init)inner.bundle_param") - self.assertEqual(pb.constraints[4].value, edgir.AssignLit(['inner', 'bundle_param'], 31.0)) - - def test_nested_inner(self) -> None: - pb = TestInternalBlock()._elaborated_def_to_proto() - - self.assertEqual(len(pb.constraints), 3) # should not generate initializers for constructors - self.assertEqual(pb.constraints[0].name, "(init)inner_bundle.float_param") - self.assertEqual(pb.constraints[0].value, edgir.AssignRef(['inner_bundle', 'float_param'], ['bundle_param'])) - # don't care about value of literal initializers - self.assertEqual(pb.constraints[1].name, "(init)inner_bundle.a.float_param") - self.assertEqual(pb.constraints[2].name, "(init)inner_bundle.b.float_param") - - def test_default_initializer(self) -> None: - pb = TestDefaultBlock()._elaborated_def_to_proto() - - self.assertEqual(len(pb.constraints), 1) - self.assertEqual(pb.constraints[0].name, "(init)inner.inner_param") - self.assertEqual(pb.constraints[0].value, edgir.AssignLit(['inner', 'inner_param'], 3.0)) - - def test_multiple_initializer(self) -> None: - pb = TestMultipleInstantiationBlock()._elaborated_def_to_proto() - - self.assertEqual(len(pb.constraints), 2) - self.assertEqual(pb.constraints[0].name, "(init)inner1.inner_param") - self.assertEqual(pb.constraints[0].value, edgir.AssignLit(['inner1', 'inner_param'], 3.0)) - self.assertEqual(pb.constraints[1].name, "(init)inner2.inner_param") - self.assertEqual(pb.constraints[1].value, edgir.AssignLit(['inner2', 'inner_param'], 3.0)) + def test_initializer(self) -> None: + pb = TestSingleInitializerBlock()._elaborated_def_to_proto() + + self.assertEqual(len(pb.constraints), 3) + self.assertEqual(pb.constraints[0].name, "(init)bundle_port.float_param") + self.assertEqual(pb.constraints[0].value, edgir.AssignLit(["bundle_port", "float_param"], 42.0)) + self.assertEqual(pb.constraints[1].name, "(init)bundle_port.a.float_param") + self.assertEqual(pb.constraints[1].value, edgir.AssignLit(["bundle_port", "a", "float_param"], 1.0)) + self.assertEqual(pb.constraints[2].name, "(init)bundle_port.b.float_param") + self.assertEqual(pb.constraints[2].value, edgir.AssignLit(["bundle_port", "b", "float_param"], -1.0)) + + def test_nested_initializer(self) -> None: + pb = TestNestedBlock()._elaborated_def_to_proto() + + self.assertEqual(len(pb.constraints), 5) + self.assertEqual(pb.constraints[0].name, "(init)outer_bundle.float_param") + self.assertEqual(pb.constraints[0].value, edgir.AssignLit(["outer_bundle", "float_param"], 21.0)) + self.assertEqual(pb.constraints[1].name, "(init)outer_bundle.a.float_param") + self.assertEqual(pb.constraints[1].value, edgir.AssignLit(["outer_bundle", "a", "float_param"], 1.0)) + self.assertEqual(pb.constraints[2].name, "(init)outer_bundle.b.float_param") + self.assertEqual(pb.constraints[2].value, edgir.AssignLit(["outer_bundle", "b", "float_param"], -1.0)) + self.assertEqual(pb.constraints[3].name, "(init)inner.inner_param") + self.assertEqual(pb.constraints[3].value, edgir.AssignLit(["inner", "inner_param"], 62.0)) + self.assertEqual(pb.constraints[4].name, "(init)inner.bundle_param") + self.assertEqual(pb.constraints[4].value, edgir.AssignLit(["inner", "bundle_param"], 31.0)) + + def test_nested_inner(self) -> None: + pb = TestInternalBlock()._elaborated_def_to_proto() + + self.assertEqual(len(pb.constraints), 3) # should not generate initializers for constructors + self.assertEqual(pb.constraints[0].name, "(init)inner_bundle.float_param") + self.assertEqual(pb.constraints[0].value, edgir.AssignRef(["inner_bundle", "float_param"], ["bundle_param"])) + # don't care about value of literal initializers + self.assertEqual(pb.constraints[1].name, "(init)inner_bundle.a.float_param") + self.assertEqual(pb.constraints[2].name, "(init)inner_bundle.b.float_param") + + def test_default_initializer(self) -> None: + pb = TestDefaultBlock()._elaborated_def_to_proto() + + self.assertEqual(len(pb.constraints), 1) + self.assertEqual(pb.constraints[0].name, "(init)inner.inner_param") + self.assertEqual(pb.constraints[0].value, edgir.AssignLit(["inner", "inner_param"], 3.0)) + + def test_multiple_initializer(self) -> None: + pb = TestMultipleInstantiationBlock()._elaborated_def_to_proto() + + self.assertEqual(len(pb.constraints), 2) + self.assertEqual(pb.constraints[0].name, "(init)inner1.inner_param") + self.assertEqual(pb.constraints[0].value, edgir.AssignLit(["inner1", "inner_param"], 3.0)) + self.assertEqual(pb.constraints[1].name, "(init)inner2.inner_param") + self.assertEqual(pb.constraints[1].value, edgir.AssignLit(["inner2", "inner_param"], 3.0)) diff --git a/edg/core/test_inner_link.py b/edg/core/test_inner_link.py index 63a48d995..0360080b8 100644 --- a/edg/core/test_inner_link.py +++ b/edg/core/test_inner_link.py @@ -8,79 +8,77 @@ class TestBundleLink(Link): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.source = self.Port(TestBundleSource()) - self.sinks = self.Port(Vector(TestBundleSink())) + self.source = self.Port(TestBundleSource()) + self.sinks = self.Port(Vector(TestBundleSink())) - self.a_net = self.connect(self.source.a, self.sinks.map_extract(lambda x: x.a), - flatten=True) - self.b_net = self.connect(self.source.b, self.sinks.map_extract(lambda x: x.b), - flatten=True) + self.a_net = self.connect(self.source.a, self.sinks.map_extract(lambda x: x.a), flatten=True) + self.b_net = self.connect(self.source.b, self.sinks.map_extract(lambda x: x.b), flatten=True) class TestBundleSource(Bundle[TestBundleLink]): - link_type = TestBundleLink + link_type = TestBundleLink - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.a = self.Port(TestPortSource()) - self.b = self.Port(TestPortSource()) + self.a = self.Port(TestPortSource()) + self.b = self.Port(TestPortSource()) class TestBundleSink(Bundle[TestBundleLink]): - link_type = TestBundleLink + link_type = TestBundleLink - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.a = self.Port(TestPortSink()) - self.b = self.Port(TestPortSink()) + self.a = self.Port(TestPortSink()) + self.b = self.Port(TestPortSink()) class InnerLinkTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestBundleLink()._elaborated_def_to_proto() - - def test_inner_links(self) -> None: - self.assertEqual(self.pb.links[0].name, 'a_net') - self.assertEqual(self.pb.links[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestLink") - self.assertEqual(self.pb.links[1].name, 'b_net') - self.assertEqual(self.pb.links[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestLink") - - def test_connects(self) -> None: - self.assertEqual(len(self.pb.constraints), 6) # TODO: maybe filter by connection types in future for robustness - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'source' - expected_conn.exported.exterior_port.ref.steps.add().name = 'a' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'a_net' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.exportedArray.exterior_port.map_extract.container.ref.steps.add().name = 'sinks' - expected_conn.exportedArray.exterior_port.map_extract.path.steps.add().name = 'a' - expected_conn.exportedArray.internal_block_port.ref.steps.add().name = 'a_net' - expected_conn.exportedArray.internal_block_port.ref.steps.add().name = 'sinks' - expected_conn.exportedArray.internal_block_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'source' - expected_conn.exported.exterior_port.ref.steps.add().name = 'b' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'b_net' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.exportedArray.exterior_port.map_extract.container.ref.steps.add().name = 'sinks' - expected_conn.exportedArray.exterior_port.map_extract.path.steps.add().name = 'b' - expected_conn.exportedArray.internal_block_port.ref.steps.add().name = 'b_net' - expected_conn.exportedArray.internal_block_port.ref.steps.add().name = 'sinks' - expected_conn.exportedArray.internal_block_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) + @override + def setUp(self) -> None: + self.pb = TestBundleLink()._elaborated_def_to_proto() + + def test_inner_links(self) -> None: + self.assertEqual(self.pb.links[0].name, "a_net") + self.assertEqual(self.pb.links[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestLink") + self.assertEqual(self.pb.links[1].name, "b_net") + self.assertEqual(self.pb.links[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestLink") + + def test_connects(self) -> None: + self.assertEqual(len(self.pb.constraints), 6) # TODO: maybe filter by connection types in future for robustness + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "source" + expected_conn.exported.exterior_port.ref.steps.add().name = "a" + expected_conn.exported.internal_block_port.ref.steps.add().name = "a_net" + expected_conn.exported.internal_block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.exportedArray.exterior_port.map_extract.container.ref.steps.add().name = "sinks" + expected_conn.exportedArray.exterior_port.map_extract.path.steps.add().name = "a" + expected_conn.exportedArray.internal_block_port.ref.steps.add().name = "a_net" + expected_conn.exportedArray.internal_block_port.ref.steps.add().name = "sinks" + expected_conn.exportedArray.internal_block_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.exported.exterior_port.ref.steps.add().name = "source" + expected_conn.exported.exterior_port.ref.steps.add().name = "b" + expected_conn.exported.internal_block_port.ref.steps.add().name = "b_net" + expected_conn.exported.internal_block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.exportedArray.exterior_port.map_extract.container.ref.steps.add().name = "sinks" + expected_conn.exportedArray.exterior_port.map_extract.path.steps.add().name = "b" + expected_conn.exportedArray.internal_block_port.ref.steps.add().name = "b_net" + expected_conn.exportedArray.internal_block_port.ref.steps.add().name = "sinks" + expected_conn.exportedArray.internal_block_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) diff --git a/edg/core/test_link.py b/edg/core/test_link.py index 0a8f3e4b5..673104b00 100644 --- a/edg/core/test_link.py +++ b/edg/core/test_link.py @@ -8,70 +8,72 @@ class LinkTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestLink()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = TestLink()._elaborated_def_to_proto() - def test_self_class(self) -> None: - self.assertEqual(self.pb.self_class.target.name, "edg.core.test_elaboration_common.TestLink") + def test_self_class(self) -> None: + self.assertEqual(self.pb.self_class.target.name, "edg.core.test_elaboration_common.TestLink") - def test_superclasses(self) -> None: - self.assertEqual(len(self.pb.superclasses), 1) - self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_elaboration_common.TestLinkBase") - self.assertEqual(len(self.pb.super_superclasses), 0) + def test_superclasses(self) -> None: + self.assertEqual(len(self.pb.superclasses), 1) + self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_elaboration_common.TestLinkBase") + self.assertEqual(len(self.pb.super_superclasses), 0) - def test_param_def(self) -> None: - self.assertEqual(len(self.pb.params), 3) - self.assertTrue(self.pb.params[0].name, 'float_param_sink_sum') - self.assertTrue(self.pb.params[0].value.HasField('floating')) - self.assertTrue(self.pb.params[1].name, 'float_param_sink_range') - self.assertTrue(self.pb.params[1].value.HasField('range')) - self.assertTrue(self.pb.params[2].name, 'range_param_sink_common') - self.assertTrue(self.pb.params[2].value.HasField('range')) + def test_param_def(self) -> None: + self.assertEqual(len(self.pb.params), 3) + self.assertTrue(self.pb.params[0].name, "float_param_sink_sum") + self.assertTrue(self.pb.params[0].value.HasField("floating")) + self.assertTrue(self.pb.params[1].name, "float_param_sink_range") + self.assertTrue(self.pb.params[1].value.HasField("range")) + self.assertTrue(self.pb.params[2].name, "range_param_sink_common") + self.assertTrue(self.pb.params[2].value.HasField("range")) - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 2) - self.assertTrue(self.pb.ports[0].name, 'source') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSource") - self.assertTrue(self.pb.ports[1].name, 'sinks') - self.assertEqual(self.pb.ports[1].value.array.self_class.target.name, "edg.core.test_elaboration_common.TestPortSink") + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 2) + self.assertTrue(self.pb.ports[0].name, "source") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSource") + self.assertTrue(self.pb.ports[1].name, "sinks") + self.assertEqual( + self.pb.ports[1].value.array.self_class.target.name, "edg.core.test_elaboration_common.TestPortSink" + ) - def test_constraints(self) -> None: - # partial test of constraints, only the ones that are more interesting than tested elsewhere - # namely, ones that deal with map and reduce operations - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + def test_constraints(self) -> None: + # partial test of constraints, only the ones that are more interesting than tested elsewhere + # namely, ones that deal with map and reduce operations + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - expected_constr = edgir.ValueExpr() - expected_constr.assign.dst.steps.add().name = 'float_param_sink_range' - expected_constr.assign.src.binary.op = edgir.BinaryExpr.RANGE + expected_constr = edgir.ValueExpr() + expected_constr.assign.dst.steps.add().name = "float_param_sink_range" + expected_constr.assign.src.binary.op = edgir.BinaryExpr.RANGE - expected_constr.assign.src.binary.lhs.unary_set.op = edgir.UnarySetExpr.MINIMUM - expected_constr.assign.src.binary.lhs.unary_set.vals.map_extract.container.ref.steps.add().name = 'sinks' - expected_constr.assign.src.binary.lhs.unary_set.vals.map_extract.path.steps.add().name = 'float_param' + expected_constr.assign.src.binary.lhs.unary_set.op = edgir.UnarySetExpr.MINIMUM + expected_constr.assign.src.binary.lhs.unary_set.vals.map_extract.container.ref.steps.add().name = "sinks" + expected_constr.assign.src.binary.lhs.unary_set.vals.map_extract.path.steps.add().name = "float_param" - expected_constr.assign.src.binary.rhs.unary_set.op = edgir.UnarySetExpr.MAXIMUM - expected_constr.assign.src.binary.rhs.unary_set.vals.map_extract.container.ref.steps.add().name = 'sinks' - expected_constr.assign.src.binary.rhs.unary_set.vals.map_extract.path.steps.add().name = 'float_param' - self.assertIn(expected_constr, constraints) + expected_constr.assign.src.binary.rhs.unary_set.op = edgir.UnarySetExpr.MAXIMUM + expected_constr.assign.src.binary.rhs.unary_set.vals.map_extract.container.ref.steps.add().name = "sinks" + expected_constr.assign.src.binary.rhs.unary_set.vals.map_extract.path.steps.add().name = "float_param" + self.assertIn(expected_constr, constraints) - expected_constr = edgir.ValueExpr() - expected_constr.binary.op = edgir.BinaryExpr.WITHIN - expected_constr.binary.lhs.ref.steps.add().name = 'source' - expected_constr.binary.lhs.ref.steps.add().name = 'float_param' - expected_constr.binary.rhs.ref.steps.add().name = 'source' - expected_constr.binary.rhs.ref.steps.add().name = 'float_param_limit' - self.assertIn(expected_constr, constraints) + expected_constr = edgir.ValueExpr() + expected_constr.binary.op = edgir.BinaryExpr.WITHIN + expected_constr.binary.lhs.ref.steps.add().name = "source" + expected_constr.binary.lhs.ref.steps.add().name = "float_param" + expected_constr.binary.rhs.ref.steps.add().name = "source" + expected_constr.binary.rhs.ref.steps.add().name = "float_param_limit" + self.assertIn(expected_constr, constraints) - expected_constr = edgir.ValueExpr() - expected_constr.assign.dst.steps.add().name = 'range_param_sink_common' - expected_constr.assign.src.unary_set.op = edgir.UnarySetExpr.INTERSECTION - expected_constr.assign.src.unary_set.vals.map_extract.container.ref.steps.add().name = 'sinks' - expected_constr.assign.src.unary_set.vals.map_extract.path.steps.add().name = 'range_limit' - self.assertIn(expected_constr, constraints) + expected_constr = edgir.ValueExpr() + expected_constr.assign.dst.steps.add().name = "range_param_sink_common" + expected_constr.assign.src.unary_set.op = edgir.UnarySetExpr.INTERSECTION + expected_constr.assign.src.unary_set.vals.map_extract.container.ref.steps.add().name = "sinks" + expected_constr.assign.src.unary_set.vals.map_extract.path.steps.add().name = "range_limit" + self.assertIn(expected_constr, constraints) - expected_constr = edgir.ValueExpr() - expected_constr.binary.op = edgir.BinaryExpr.WITHIN - expected_constr.binary.lhs.ref.steps.add().name = 'source' - expected_constr.binary.lhs.ref.steps.add().name = 'range_param' - expected_constr.binary.rhs.ref.steps.add().name = 'range_param_sink_common' - self.assertIn(expected_constr, constraints) + expected_constr = edgir.ValueExpr() + expected_constr.binary.op = edgir.BinaryExpr.WITHIN + expected_constr.binary.lhs.ref.steps.add().name = "source" + expected_constr.binary.lhs.ref.steps.add().name = "range_param" + expected_constr.binary.rhs.ref.steps.add().name = "range_param_sink_common" + self.assertIn(expected_constr, constraints) diff --git a/edg/core/test_mixin.py b/edg/core/test_mixin.py index 7c33a51eb..e757f4f3d 100644 --- a/edg/core/test_mixin.py +++ b/edg/core/test_mixin.py @@ -10,140 +10,140 @@ @abstract_block class TestMixinBase(Block): - def __init__(self) -> None: - super().__init__() - self.base_port = self.Port(TestPortSink()) + def __init__(self) -> None: + super().__init__() + self.base_port = self.Port(TestPortSink()) class TestMixin(BlockInterfaceMixin[TestMixinBase]): - def __init__(self, *args: Any, mixin_float: FloatLike = 1.0, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.mixin_port = self.Port(TestPortSink()) - self.mixin_float = self.ArgParameter(mixin_float) + def __init__(self, *args: Any, mixin_float: FloatLike = 1.0, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.mixin_port = self.Port(TestPortSink()) + self.mixin_float = self.ArgParameter(mixin_float) class MixinProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - cls = TestMixin() - self.assertEqual(cls._is_mixin(), True) - self.pb = cls._elaborated_def_to_proto() - - def test_abstract(self) -> None: - self.assertEqual(self.pb.is_abstract, True) - self.assertEqual(self.pb.HasField('default_refinement'), False) - - def test_superclass(self) -> None: - self.assertEqual(self.pb.self_class.target.name, "edg.core.test_mixin.TestMixin") - self.assertEqual(self.pb.prerefine_class.target.name, "edg.core.test_mixin.TestMixin") - self.assertEqual(len(self.pb.superclasses), 1) - self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_mixin.TestMixinBase") - self.assertEqual(len(self.pb.super_superclasses), 0) - - def test_param_def(self) -> None: - self.assertEqual(len(self.pb.params), 1) - self.assertEqual(self.pb.params[0].name, 'mixin_float') - self.assertTrue(self.pb.params[0].value.HasField('floating')) - - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 1) - self.assertEqual(self.pb.ports[0].name, 'mixin_port') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") - - def test_connected_constraint(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.ref.steps.add().name = 'mixin_port' - expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED - self.assertEqual(self.pb.constraints[0].name, "(reqd)mixin_port") - self.assertEqual(self.pb.constraints[0].value, expected_constr) + @override + def setUp(self) -> None: + cls = TestMixin() + self.assertEqual(cls._is_mixin(), True) + self.pb = cls._elaborated_def_to_proto() + + def test_abstract(self) -> None: + self.assertEqual(self.pb.is_abstract, True) + self.assertEqual(self.pb.HasField("default_refinement"), False) + + def test_superclass(self) -> None: + self.assertEqual(self.pb.self_class.target.name, "edg.core.test_mixin.TestMixin") + self.assertEqual(self.pb.prerefine_class.target.name, "edg.core.test_mixin.TestMixin") + self.assertEqual(len(self.pb.superclasses), 1) + self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_mixin.TestMixinBase") + self.assertEqual(len(self.pb.super_superclasses), 0) + + def test_param_def(self) -> None: + self.assertEqual(len(self.pb.params), 1) + self.assertEqual(self.pb.params[0].name, "mixin_float") + self.assertTrue(self.pb.params[0].value.HasField("floating")) + + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 1) + self.assertEqual(self.pb.ports[0].name, "mixin_port") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") + + def test_connected_constraint(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.ref.steps.add().name = "mixin_port" + expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED + self.assertEqual(self.pb.constraints[0].name, "(reqd)mixin_port") + self.assertEqual(self.pb.constraints[0].value, expected_constr) class TestMixinSubclass(TestMixin): - pass + pass class MixinSubclassProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - cls = TestMixinSubclass() - self.assertEqual(cls._is_mixin(), True) - self.pb = cls._elaborated_def_to_proto() - - def test_abstract(self) -> None: - self.assertEqual(self.pb.is_abstract, True) - self.assertEqual(self.pb.HasField('default_refinement'), False) - - def test_superclass(self) -> None: - self.assertEqual(self.pb.self_class.target.name, "edg.core.test_mixin.TestMixinSubclass") - self.assertEqual(self.pb.prerefine_class.target.name, "edg.core.test_mixin.TestMixinSubclass") - self.assertEqual(len(self.pb.superclasses), 1) - self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_mixin.TestMixin") - self.assertEqual(len(self.pb.super_superclasses), 1) - self.assertEqual(self.pb.super_superclasses[0].target.name, "edg.core.test_mixin.TestMixinBase") - - # the rest of this tests that it has inherited the mixin's base port and param - def test_param_def(self) -> None: - self.assertEqual(len(self.pb.params), 1) - self.assertEqual(self.pb.params[0].name, 'mixin_float') - self.assertTrue(self.pb.params[0].value.HasField('floating')) - - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 1) - self.assertEqual(self.pb.ports[0].name, 'mixin_port') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") - - def test_connected_constraint(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.ref.steps.add().name = 'mixin_port' - expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED - self.assertEqual(self.pb.constraints[0].name, "(reqd)mixin_port") - self.assertEqual(self.pb.constraints[0].value, expected_constr) + @override + def setUp(self) -> None: + cls = TestMixinSubclass() + self.assertEqual(cls._is_mixin(), True) + self.pb = cls._elaborated_def_to_proto() + + def test_abstract(self) -> None: + self.assertEqual(self.pb.is_abstract, True) + self.assertEqual(self.pb.HasField("default_refinement"), False) + + def test_superclass(self) -> None: + self.assertEqual(self.pb.self_class.target.name, "edg.core.test_mixin.TestMixinSubclass") + self.assertEqual(self.pb.prerefine_class.target.name, "edg.core.test_mixin.TestMixinSubclass") + self.assertEqual(len(self.pb.superclasses), 1) + self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_mixin.TestMixin") + self.assertEqual(len(self.pb.super_superclasses), 1) + self.assertEqual(self.pb.super_superclasses[0].target.name, "edg.core.test_mixin.TestMixinBase") + + # the rest of this tests that it has inherited the mixin's base port and param + def test_param_def(self) -> None: + self.assertEqual(len(self.pb.params), 1) + self.assertEqual(self.pb.params[0].name, "mixin_float") + self.assertTrue(self.pb.params[0].value.HasField("floating")) + + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 1) + self.assertEqual(self.pb.ports[0].name, "mixin_port") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") + + def test_connected_constraint(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.ref.steps.add().name = "mixin_port" + expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED + self.assertEqual(self.pb.constraints[0].name, "(reqd)mixin_port") + self.assertEqual(self.pb.constraints[0].value, expected_constr) class TestMixinConcreteBlock(TestMixin, TestMixinBase): - pass # doesn't define anything + pass # doesn't define anything class MixinConcreteBlockProtoTestCase(unittest.TestCase): # pretty straightforward test of Python inheritance - @override - def setUp(self) -> None: - cls = TestMixinConcreteBlock() - self.assertEqual(cls._is_mixin(), False) - self.pb = cls._elaborated_def_to_proto() - - def test_abstract(self) -> None: - self.assertEqual(self.pb.is_abstract, False) - self.assertEqual(self.pb.HasField('default_refinement'), False) - - def test_superclass(self) -> None: - self.assertEqual(self.pb.self_class.target.name, "edg.core.test_mixin.TestMixinConcreteBlock") - self.assertEqual(self.pb.prerefine_class.target.name, "edg.core.test_mixin.TestMixinConcreteBlock") - self.assertEqual(len(self.pb.superclasses), 2) - self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_mixin.TestMixin") - self.assertEqual(self.pb.superclasses[1].target.name, "edg.core.test_mixin.TestMixinBase") - - # the rest of this tests that it has inherited the mixin's base port and param - def test_param_def(self) -> None: - self.assertEqual(len(self.pb.params), 1) - self.assertEqual(self.pb.params[0].name, 'mixin_float') - self.assertTrue(self.pb.params[0].value.HasField('floating')) - - def test_port_def(self) -> None: - self.assertEqual(len(self.pb.ports), 2) - self.assertEqual(self.pb.ports[0].name, 'base_port') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") - self.assertEqual(self.pb.ports[1].name, 'mixin_port') - self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") - - def test_connected_constraint(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.ref.steps.add().name = 'base_port' - expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED - self.assertEqual(self.pb.constraints[0].name, "(reqd)base_port") - self.assertEqual(self.pb.constraints[0].value, expected_constr) - - expected_constr = edgir.ValueExpr() - expected_constr.ref.steps.add().name = 'mixin_port' - expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED - self.assertEqual(self.pb.constraints[1].name, "(reqd)mixin_port") - self.assertEqual(self.pb.constraints[1].value, expected_constr) + @override + def setUp(self) -> None: + cls = TestMixinConcreteBlock() + self.assertEqual(cls._is_mixin(), False) + self.pb = cls._elaborated_def_to_proto() + + def test_abstract(self) -> None: + self.assertEqual(self.pb.is_abstract, False) + self.assertEqual(self.pb.HasField("default_refinement"), False) + + def test_superclass(self) -> None: + self.assertEqual(self.pb.self_class.target.name, "edg.core.test_mixin.TestMixinConcreteBlock") + self.assertEqual(self.pb.prerefine_class.target.name, "edg.core.test_mixin.TestMixinConcreteBlock") + self.assertEqual(len(self.pb.superclasses), 2) + self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_mixin.TestMixin") + self.assertEqual(self.pb.superclasses[1].target.name, "edg.core.test_mixin.TestMixinBase") + + # the rest of this tests that it has inherited the mixin's base port and param + def test_param_def(self) -> None: + self.assertEqual(len(self.pb.params), 1) + self.assertEqual(self.pb.params[0].name, "mixin_float") + self.assertTrue(self.pb.params[0].value.HasField("floating")) + + def test_port_def(self) -> None: + self.assertEqual(len(self.pb.ports), 2) + self.assertEqual(self.pb.ports[0].name, "base_port") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") + self.assertEqual(self.pb.ports[1].name, "mixin_port") + self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_common.TestPortSink") + + def test_connected_constraint(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.ref.steps.add().name = "base_port" + expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED + self.assertEqual(self.pb.constraints[0].name, "(reqd)base_port") + self.assertEqual(self.pb.constraints[0].value, expected_constr) + + expected_constr = edgir.ValueExpr() + expected_constr.ref.steps.add().name = "mixin_port" + expected_constr.ref.steps.add().reserved_param = edgir.IS_CONNECTED + self.assertEqual(self.pb.constraints[1].name, "(reqd)mixin_port") + self.assertEqual(self.pb.constraints[1].value, expected_constr) diff --git a/edg/core/test_mixin_usage.py b/edg/core/test_mixin_usage.py index 1256f1664..9b6c4bab0 100644 --- a/edg/core/test_mixin_usage.py +++ b/edg/core/test_mixin_usage.py @@ -9,66 +9,66 @@ class MixinUsageTestCase(unittest.TestCase): - class MixinBlock(Block): + class MixinBlock(Block): + @override + def contents(self) -> None: + super().contents() + + self.block = self.Block(TestMixinBase()) + + self.base_source = self.Block(TestBlockSource()) + self.block_net = self.connect(self.block.base_port, self.base_source.source) + + self.mixin = self.block.with_mixin(TestMixin()) + self.mixin_source = self.Block(TestBlockSource()) + self.mixin_net = self.connect(self.mixin.mixin_port, self.mixin_source.source) + @override - def contents(self) -> None: - super().contents() - - self.block = self.Block(TestMixinBase()) - - self.base_source = self.Block(TestBlockSource()) - self.block_net = self.connect(self.block.base_port, self.base_source.source) - - self.mixin = self.block.with_mixin(TestMixin()) - self.mixin_source = self.Block(TestBlockSource()) - self.mixin_net = self.connect(self.mixin.mixin_port, self.mixin_source.source) - - @override - def setUp(self) -> None: - self.pb = self.MixinBlock()._elaborated_def_to_proto() - - def test_subblock_def(self) -> None: - self.assertEqual(self.pb.blocks[0].name, 'block') - self.assertEqual(self.pb.blocks[0].value.lib_elem.base.target.name, "edg.core.test_mixin.TestMixinBase") - self.assertEqual(len(self.pb.blocks[0].value.lib_elem.mixins), 1) - self.assertEqual(self.pb.blocks[0].value.lib_elem.mixins[0].target.name, "edg.core.test_mixin.TestMixin") - - def test_connectivity(self) -> None: - self.assertEqual(len(self.pb.constraints), 5) # 4 connections + 1 initializer - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'block_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'base_source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'block_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'block' - expected_conn.connected.block_port.ref.steps.add().name = 'base_port' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'mixin_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - expected_conn.connected.block_port.ref.steps.add().name = 'mixin_source' - expected_conn.connected.block_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'mixin_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'block' - expected_conn.connected.block_port.ref.steps.add().name = 'mixin_port' - self.assertIn(expected_conn, constraints) - - def test_initializer(self) -> None: - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - # default assignment - self.assertIn(edgir.AssignLit(['block', 'mixin_float'], 1.0), constraints) + def setUp(self) -> None: + self.pb = self.MixinBlock()._elaborated_def_to_proto() + + def test_subblock_def(self) -> None: + self.assertEqual(self.pb.blocks[0].name, "block") + self.assertEqual(self.pb.blocks[0].value.lib_elem.base.target.name, "edg.core.test_mixin.TestMixinBase") + self.assertEqual(len(self.pb.blocks[0].value.lib_elem.mixins), 1) + self.assertEqual(self.pb.blocks[0].value.lib_elem.mixins[0].target.name, "edg.core.test_mixin.TestMixin") + + def test_connectivity(self) -> None: + self.assertEqual(len(self.pb.constraints), 5) # 4 connections + 1 initializer + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "block_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "base_source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "block_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "block" + expected_conn.connected.block_port.ref.steps.add().name = "base_port" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "mixin_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + expected_conn.connected.block_port.ref.steps.add().name = "mixin_source" + expected_conn.connected.block_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "mixin_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "block" + expected_conn.connected.block_port.ref.steps.add().name = "mixin_port" + self.assertIn(expected_conn, constraints) + + def test_initializer(self) -> None: + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + # default assignment + self.assertIn(edgir.AssignLit(["block", "mixin_float"], 1.0), constraints) diff --git a/edg/core/test_multibidict.py b/edg/core/test_multibidict.py index 6b8d761eb..af49db481 100644 --- a/edg/core/test_multibidict.py +++ b/edg/core/test_multibidict.py @@ -4,43 +4,43 @@ class MultiBiDictTestCase(unittest.TestCase): - def test_simple(self) -> None: - mbd: MultiBiDict[int, str] = MultiBiDict() - mbd.add(1, 'a') - mbd.add(2, 'b') - mbd.add(3, 'c') - - self.assertEqual(mbd.get(1), {'a'}) - self.assertEqual(mbd.get(2), {'b'}) - self.assertEqual(mbd.get(3), {'c'}) - - self.assertEqual(mbd.get_by_value('a'), {1}) - self.assertEqual(mbd.get_by_value('b'), {2}) - self.assertEqual(mbd.get_by_value('c'), {3}) - - def test_multi(self) -> None: - mbd: MultiBiDict[int, str] = MultiBiDict() - mbd.add(1, 'a') - mbd.add(1, 'aa') - mbd.add(2, 'b') - mbd.add(3, 'b') - - self.assertEqual(mbd.get(1), {'a', 'aa'}) - self.assertEqual(mbd.get(2), {'b'}) - self.assertEqual(mbd.get(3), {'b'}) - - self.assertEqual(mbd.get_by_value('a'), {1}) - self.assertEqual(mbd.get_by_value('aa'), {1}) - self.assertEqual(mbd.get_by_value('b'), {2, 3}) - - def test_overlap(self) -> None: - mbd: MultiBiDict[int, str] = MultiBiDict() - mbd.add(1, 'a') - mbd.add(1, 'aa') - mbd.add(2, 'aa') - - self.assertEqual(mbd.get(1), {'a', 'aa'}) - self.assertEqual(mbd.get(2), {'aa'}) - - self.assertEqual(mbd.get_by_value('a'), {1}) - self.assertEqual(mbd.get_by_value('aa'), {1, 2}) + def test_simple(self) -> None: + mbd: MultiBiDict[int, str] = MultiBiDict() + mbd.add(1, "a") + mbd.add(2, "b") + mbd.add(3, "c") + + self.assertEqual(mbd.get(1), {"a"}) + self.assertEqual(mbd.get(2), {"b"}) + self.assertEqual(mbd.get(3), {"c"}) + + self.assertEqual(mbd.get_by_value("a"), {1}) + self.assertEqual(mbd.get_by_value("b"), {2}) + self.assertEqual(mbd.get_by_value("c"), {3}) + + def test_multi(self) -> None: + mbd: MultiBiDict[int, str] = MultiBiDict() + mbd.add(1, "a") + mbd.add(1, "aa") + mbd.add(2, "b") + mbd.add(3, "b") + + self.assertEqual(mbd.get(1), {"a", "aa"}) + self.assertEqual(mbd.get(2), {"b"}) + self.assertEqual(mbd.get(3), {"b"}) + + self.assertEqual(mbd.get_by_value("a"), {1}) + self.assertEqual(mbd.get_by_value("aa"), {1}) + self.assertEqual(mbd.get_by_value("b"), {2, 3}) + + def test_overlap(self) -> None: + mbd: MultiBiDict[int, str] = MultiBiDict() + mbd.add(1, "a") + mbd.add(1, "aa") + mbd.add(2, "aa") + + self.assertEqual(mbd.get(1), {"a", "aa"}) + self.assertEqual(mbd.get(2), {"aa"}) + + self.assertEqual(mbd.get_by_value("a"), {1}) + self.assertEqual(mbd.get_by_value("aa"), {1, 2}) diff --git a/edg/core/test_multipack.py b/edg/core/test_multipack.py index bbf278e3c..1fbfb4699 100644 --- a/edg/core/test_multipack.py +++ b/edg/core/test_multipack.py @@ -8,190 +8,192 @@ class PartSink(Block): - def __init__(self) -> None: - super().__init__() - self.param = self.Parameter(FloatExpr()) - self.result_param = self.Parameter(IntExpr()) - self.sink = self.Port(TestPortSink(), optional=True) + def __init__(self) -> None: + super().__init__() + self.param = self.Parameter(FloatExpr()) + self.result_param = self.Parameter(IntExpr()) + self.sink = self.Port(TestPortSink(), optional=True) class MultipackBlockSink(MultipackBlock): - """Unlike a real multipack block, this is simplified and has no implementation.""" - def __init__(self) -> None: - super().__init__() + """Unlike a real multipack block, this is simplified and has no implementation.""" - self.sink1 = self.PackedPart(PartSink()) - self.sink2 = self.PackedPart(PartSink()) - self.sink_port1 = self.PackedExport(self.sink1.sink) - self.sink_port2 = self.PackedExport(self.sink2.sink) - self.param1 = self.PackedParameter(self.sink1.param) - self.param2 = self.PackedParameter(self.sink2.param) + def __init__(self) -> None: + super().__init__() - self.result_param = self.Parameter(IntExpr()) - self.unpacked_assign(self.sink1.result_param, self.result_param) - self.unpacked_assign(self.sink2.result_param, self.result_param) + self.sink1 = self.PackedPart(PartSink()) + self.sink2 = self.PackedPart(PartSink()) + self.sink_port1 = self.PackedExport(self.sink1.sink) + self.sink_port2 = self.PackedExport(self.sink2.sink) + self.param1 = self.PackedParameter(self.sink1.param) + self.param2 = self.PackedParameter(self.sink2.param) + + self.result_param = self.Parameter(IntExpr()) + self.unpacked_assign(self.sink1.result_param, self.result_param) + self.unpacked_assign(self.sink2.result_param, self.result_param) class TestBlockContainerSink(Block): - def __init__(self) -> None: - super().__init__() - self.inner = self.Block(PartSink()) - self.param = self.Parameter(FloatExpr()) - self.sink = self.Export(self.inner.sink) - self.assign(self.inner.param, self.param) + def __init__(self) -> None: + super().__init__() + self.inner = self.Block(PartSink()) + self.param = self.Parameter(FloatExpr()) + self.sink = self.Export(self.inner.sink) + self.assign(self.inner.param, self.param) class TopMultipackDesign(DesignTop): - @override - def contents(self) -> None: - super().contents() - self.sink1 = self.Block(PartSink()) - self.sink2 = self.Block(TestBlockContainerSink()) + @override + def contents(self) -> None: + super().contents() + self.sink1 = self.Block(PartSink()) + self.sink2 = self.Block(TestBlockContainerSink()) - @override - def multipack(self) -> None: - self.packed = self.Block(MultipackBlockSink()) - self.pack(self.packed.sink1, ['sink1']) - self.pack(self.packed.sink2, ['sink2', 'inner']) + @override + def multipack(self) -> None: + self.packed = self.Block(MultipackBlockSink()) + self.pack(self.packed.sink1, ["sink1"]) + self.pack(self.packed.sink2, ["sink2", "inner"]) class TopMultipackDesignTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - pb = TopMultipackDesign()._elaborated_def_to_proto() - self.constraints = list(map(lambda pair: pair.value, pb.constraints)) - - def test_constraints_count(self) -> None: # so individual cases (export / assigns) can still pass - self.assertEqual(len(self.constraints), 6) - - def test_export_tunnel(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = 'packed' - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = 'sink_port1' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'sink1' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'sink' - self.assertIn(expected_constr, self.constraints) - - expected_constr = edgir.ValueExpr() - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = 'packed' - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = 'sink_port2' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'sink2' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'inner' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'sink' - self.assertIn(expected_constr, self.constraints) - - def test_assign_tunnel(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.assignTunnel.dst.steps.add().name = 'packed' - expected_constr.assignTunnel.dst.steps.add().name = 'param1' - expected_constr.assignTunnel.src.ref.steps.add().name = 'sink1' - expected_constr.assignTunnel.src.ref.steps.add().name = 'param' - self.assertIn(expected_constr, self.constraints) - - expected_constr = edgir.ValueExpr() - expected_constr.assignTunnel.dst.steps.add().name = 'packed' - expected_constr.assignTunnel.dst.steps.add().name = 'param2' - expected_constr.assignTunnel.src.ref.steps.add().name = 'sink2' - expected_constr.assignTunnel.src.ref.steps.add().name = 'inner' - expected_constr.assignTunnel.src.ref.steps.add().name = 'param' - self.assertIn(expected_constr, self.constraints) - - def test_assign_unpacked_tunnel(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.assignTunnel.dst.steps.add().name = 'sink1' - expected_constr.assignTunnel.dst.steps.add().name = 'result_param' - expected_constr.assignTunnel.src.ref.steps.add().name = 'packed' - expected_constr.assignTunnel.src.ref.steps.add().name = 'result_param' - self.assertIn(expected_constr, self.constraints) - - expected_constr = edgir.ValueExpr() - expected_constr.assignTunnel.dst.steps.add().name = 'sink2' - expected_constr.assignTunnel.dst.steps.add().name = 'inner' - expected_constr.assignTunnel.dst.steps.add().name = 'result_param' - expected_constr.assignTunnel.src.ref.steps.add().name = 'packed' - expected_constr.assignTunnel.src.ref.steps.add().name = 'result_param' - self.assertIn(expected_constr, self.constraints) + @override + def setUp(self) -> None: + pb = TopMultipackDesign()._elaborated_def_to_proto() + self.constraints = list(map(lambda pair: pair.value, pb.constraints)) + + def test_constraints_count(self) -> None: # so individual cases (export / assigns) can still pass + self.assertEqual(len(self.constraints), 6) + + def test_export_tunnel(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = "packed" + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = "sink_port1" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "sink1" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "sink" + self.assertIn(expected_constr, self.constraints) + + expected_constr = edgir.ValueExpr() + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = "packed" + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = "sink_port2" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "sink2" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "inner" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "sink" + self.assertIn(expected_constr, self.constraints) + + def test_assign_tunnel(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.assignTunnel.dst.steps.add().name = "packed" + expected_constr.assignTunnel.dst.steps.add().name = "param1" + expected_constr.assignTunnel.src.ref.steps.add().name = "sink1" + expected_constr.assignTunnel.src.ref.steps.add().name = "param" + self.assertIn(expected_constr, self.constraints) + + expected_constr = edgir.ValueExpr() + expected_constr.assignTunnel.dst.steps.add().name = "packed" + expected_constr.assignTunnel.dst.steps.add().name = "param2" + expected_constr.assignTunnel.src.ref.steps.add().name = "sink2" + expected_constr.assignTunnel.src.ref.steps.add().name = "inner" + expected_constr.assignTunnel.src.ref.steps.add().name = "param" + self.assertIn(expected_constr, self.constraints) + + def test_assign_unpacked_tunnel(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.assignTunnel.dst.steps.add().name = "sink1" + expected_constr.assignTunnel.dst.steps.add().name = "result_param" + expected_constr.assignTunnel.src.ref.steps.add().name = "packed" + expected_constr.assignTunnel.src.ref.steps.add().name = "result_param" + self.assertIn(expected_constr, self.constraints) + + expected_constr = edgir.ValueExpr() + expected_constr.assignTunnel.dst.steps.add().name = "sink2" + expected_constr.assignTunnel.dst.steps.add().name = "inner" + expected_constr.assignTunnel.dst.steps.add().name = "result_param" + expected_constr.assignTunnel.src.ref.steps.add().name = "packed" + expected_constr.assignTunnel.src.ref.steps.add().name = "result_param" + self.assertIn(expected_constr, self.constraints) class MultipackArrayBlockSink(MultipackBlock): - """Same as above, but with array constructs.""" - def __init__(self) -> None: - super().__init__() - self.sinks = self.PackedPart(PackedBlockArray(PartSink())) - self.sink_ports = self.PackedExport(self.sinks.ports_array(lambda x: x.sink)) - self.params = self.PackedParameter(self.sinks.params_array(lambda x: x.param)) + """Same as above, but with array constructs.""" + + def __init__(self) -> None: + super().__init__() + self.sinks = self.PackedPart(PackedBlockArray(PartSink())) + self.sink_ports = self.PackedExport(self.sinks.ports_array(lambda x: x.sink)) + self.params = self.PackedParameter(self.sinks.params_array(lambda x: x.param)) - self.result_param = self.Parameter(IntExpr()) - self.unpacked_assign(self.sinks.params(lambda x: x.result_param), self.result_param) + self.result_param = self.Parameter(IntExpr()) + self.unpacked_assign(self.sinks.params(lambda x: x.result_param), self.result_param) class TopMultipackArrayDesign(DesignTop): - @override - def contents(self) -> None: - super().contents() - self.sink1 = self.Block(PartSink()) - self.sink2 = self.Block(TestBlockContainerSink()) + @override + def contents(self) -> None: + super().contents() + self.sink1 = self.Block(PartSink()) + self.sink2 = self.Block(TestBlockContainerSink()) - @override - def multipack(self) -> None: - self.packed = self.Block(MultipackArrayBlockSink()) - self.pack(self.packed.sinks.request('1'), ['sink1']) - self.pack(self.packed.sinks.request('2'), ['sink2', 'inner']) + @override + def multipack(self) -> None: + self.packed = self.Block(MultipackArrayBlockSink()) + self.pack(self.packed.sinks.request("1"), ["sink1"]) + self.pack(self.packed.sinks.request("2"), ["sink2", "inner"]) class TopMultipackArrayDesignTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - pb = TopMultipackArrayDesign()._elaborated_def_to_proto() - self.constraints = list(map(lambda pair: pair.value, pb.constraints)) - - def test_constraints_count(self) -> None: # so individual cases (export / assigns) can still pass - self.assertEqual(len(self.constraints), 5) - - def test_export_tunnel(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = 'packed' - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = 'sink_ports' - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().allocate = '1' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'sink1' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'sink' - self.assertIn(expected_constr, self.constraints) - - expected_constr = edgir.ValueExpr() - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = 'packed' - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = 'sink_ports' - expected_constr.exportedTunnel.internal_block_port.ref.steps.add().allocate = '2' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'sink2' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'inner' - expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = 'sink' - self.assertIn(expected_constr, self.constraints) - - def test_assign_tunnel(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.assignTunnel.dst.steps.add().name = 'packed' - expected_constr.assignTunnel.dst.steps.add().name = 'params' - expected_array = expected_constr.assignTunnel.src.array - expected_ref1 = expected_array.vals.add().ref - expected_ref1.steps.add().name = 'sink1' - expected_ref1.steps.add().name = 'param' - expected_ref2 = expected_array.vals.add().ref - expected_ref2.steps.add().name = 'sink2' - expected_ref2.steps.add().name = 'inner' - expected_ref2.steps.add().name = 'param' - self.assertIn(expected_constr, self.constraints) - - def test_assign_unpacked_tunnel(self) -> None: - expected_constr = edgir.ValueExpr() - expected_constr.assignTunnel.dst.steps.add().name = 'sink1' - expected_constr.assignTunnel.dst.steps.add().name = 'result_param' - expected_constr.assignTunnel.src.ref.steps.add().name = 'packed' - expected_constr.assignTunnel.src.ref.steps.add().name = 'result_param' - self.assertIn(expected_constr, self.constraints) - - expected_constr = edgir.ValueExpr() - expected_constr.assignTunnel.dst.steps.add().name = 'sink2' - expected_constr.assignTunnel.dst.steps.add().name = 'inner' - expected_constr.assignTunnel.dst.steps.add().name = 'result_param' - expected_constr.assignTunnel.src.ref.steps.add().name = 'packed' - expected_constr.assignTunnel.src.ref.steps.add().name = 'result_param' - self.assertIn(expected_constr, self.constraints) \ No newline at end of file + @override + def setUp(self) -> None: + pb = TopMultipackArrayDesign()._elaborated_def_to_proto() + self.constraints = list(map(lambda pair: pair.value, pb.constraints)) + + def test_constraints_count(self) -> None: # so individual cases (export / assigns) can still pass + self.assertEqual(len(self.constraints), 5) + + def test_export_tunnel(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = "packed" + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = "sink_ports" + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().allocate = "1" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "sink1" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "sink" + self.assertIn(expected_constr, self.constraints) + + expected_constr = edgir.ValueExpr() + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = "packed" + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().name = "sink_ports" + expected_constr.exportedTunnel.internal_block_port.ref.steps.add().allocate = "2" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "sink2" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "inner" + expected_constr.exportedTunnel.exterior_port.ref.steps.add().name = "sink" + self.assertIn(expected_constr, self.constraints) + + def test_assign_tunnel(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.assignTunnel.dst.steps.add().name = "packed" + expected_constr.assignTunnel.dst.steps.add().name = "params" + expected_array = expected_constr.assignTunnel.src.array + expected_ref1 = expected_array.vals.add().ref + expected_ref1.steps.add().name = "sink1" + expected_ref1.steps.add().name = "param" + expected_ref2 = expected_array.vals.add().ref + expected_ref2.steps.add().name = "sink2" + expected_ref2.steps.add().name = "inner" + expected_ref2.steps.add().name = "param" + self.assertIn(expected_constr, self.constraints) + + def test_assign_unpacked_tunnel(self) -> None: + expected_constr = edgir.ValueExpr() + expected_constr.assignTunnel.dst.steps.add().name = "sink1" + expected_constr.assignTunnel.dst.steps.add().name = "result_param" + expected_constr.assignTunnel.src.ref.steps.add().name = "packed" + expected_constr.assignTunnel.src.ref.steps.add().name = "result_param" + self.assertIn(expected_constr, self.constraints) + + expected_constr = edgir.ValueExpr() + expected_constr.assignTunnel.dst.steps.add().name = "sink2" + expected_constr.assignTunnel.dst.steps.add().name = "inner" + expected_constr.assignTunnel.dst.steps.add().name = "result_param" + expected_constr.assignTunnel.src.ref.steps.add().name = "packed" + expected_constr.assignTunnel.src.ref.steps.add().name = "result_param" + self.assertIn(expected_constr, self.constraints) diff --git a/edg/core/test_path.py b/edg/core/test_path.py index 86fd8ebab..b18521612 100644 --- a/edg/core/test_path.py +++ b/edg/core/test_path.py @@ -4,18 +4,22 @@ class PathTestCase(unittest.TestCase): - def test_startswith(self) -> None: - path = Path.empty() - self.assertTrue(path.append_block('a', 'b').startswith(path.append_block('a'))) - self.assertFalse(path.append_block('a').startswith(path.append_block('a', 'b'))) - self.assertTrue(path.append_block('a').startswith(path.append_block('a'))) - self.assertTrue(path.append_block('a', 'b').startswith(path.append_block('a', 'b'))) + def test_startswith(self) -> None: + path = Path.empty() + self.assertTrue(path.append_block("a", "b").startswith(path.append_block("a"))) + self.assertFalse(path.append_block("a").startswith(path.append_block("a", "b"))) + self.assertTrue(path.append_block("a").startswith(path.append_block("a"))) + self.assertTrue(path.append_block("a", "b").startswith(path.append_block("a", "b"))) - self.assertFalse(path.append_block('a').startswith(path.append_link('a'))) + self.assertFalse(path.append_block("a").startswith(path.append_link("a"))) - self.assertTrue(path.append_block('a').append_link('b').startswith(path.append_block('a'))) - self.assertTrue(path.append_block('a').append_link('b').startswith(path.append_block('a').append_link('b'))) - self.assertTrue(path.append_block('a').append_link('b', 'c').startswith(path.append_block('a').append_link('b'))) - self.assertTrue(path.append_block('a').append_link('b', 'c').startswith(path.append_block('a').append_link('b', 'c'))) - self.assertFalse(path.append_block('a').append_link('b').startswith(path.append_link('b'))) - self.assertFalse(path.append_block('a').append_link('b').startswith(path.append_block('a', 'b'))) + self.assertTrue(path.append_block("a").append_link("b").startswith(path.append_block("a"))) + self.assertTrue(path.append_block("a").append_link("b").startswith(path.append_block("a").append_link("b"))) + self.assertTrue( + path.append_block("a").append_link("b", "c").startswith(path.append_block("a").append_link("b")) + ) + self.assertTrue( + path.append_block("a").append_link("b", "c").startswith(path.append_block("a").append_link("b", "c")) + ) + self.assertFalse(path.append_block("a").append_link("b").startswith(path.append_link("b"))) + self.assertFalse(path.append_block("a").append_link("b").startswith(path.append_block("a", "b"))) diff --git a/edg/core/test_port.py b/edg/core/test_port.py index a790a6b01..f37f259fd 100644 --- a/edg/core/test_port.py +++ b/edg/core/test_port.py @@ -8,33 +8,33 @@ class PortProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = cast(edgir.Port, TestPortBase()._def_to_proto()) # TODO eliminate cast + @override + def setUp(self) -> None: + self.pb = cast(edgir.Port, TestPortBase()._def_to_proto()) # TODO eliminate cast - def test_contains_param(self) -> None: - self.assertEqual(len(self.pb.params), 1) - self.assertEqual(self.pb.params[0].name, 'float_param') - self.assertTrue(self.pb.params[0].value.HasField('floating')) + def test_contains_param(self) -> None: + self.assertEqual(len(self.pb.params), 1) + self.assertEqual(self.pb.params[0].name, "float_param") + self.assertTrue(self.pb.params[0].value.HasField("floating")) class PortSourceProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = cast(edgir.Port, TestPortSource()._def_to_proto()) - - def test_self_class(self) -> None: - self.assertEqual(self.pb.self_class.target.name, "edg.core.test_elaboration_common.TestPortSource") - - def test_superclasses(self) -> None: - self.assertEqual(len(self.pb.superclasses), 1) - self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_elaboration_common.TestPortBase") - - def test_contains_param(self) -> None: - self.assertEqual(len(self.pb.params), 3) - self.assertTrue(self.pb.params[0].name, 'float_param') - self.assertTrue(self.pb.params[0].value.HasField('floating')) - self.assertTrue(self.pb.params[1].name, 'float_param_limit') - self.assertTrue(self.pb.params[1].value.HasField('range')) - self.assertTrue(self.pb.params[2].name, 'range_param') - self.assertTrue(self.pb.params[2].value.HasField('range')) + @override + def setUp(self) -> None: + self.pb = cast(edgir.Port, TestPortSource()._def_to_proto()) + + def test_self_class(self) -> None: + self.assertEqual(self.pb.self_class.target.name, "edg.core.test_elaboration_common.TestPortSource") + + def test_superclasses(self) -> None: + self.assertEqual(len(self.pb.superclasses), 1) + self.assertEqual(self.pb.superclasses[0].target.name, "edg.core.test_elaboration_common.TestPortBase") + + def test_contains_param(self) -> None: + self.assertEqual(len(self.pb.params), 3) + self.assertTrue(self.pb.params[0].name, "float_param") + self.assertTrue(self.pb.params[0].value.HasField("floating")) + self.assertTrue(self.pb.params[1].name, "float_param_limit") + self.assertTrue(self.pb.params[1].value.HasField("range")) + self.assertTrue(self.pb.params[2].name, "range_param") + self.assertTrue(self.pb.params[2].value.HasField("range")) diff --git a/edg/core/test_port_adapter.py b/edg/core/test_port_adapter.py index c3eb88fb1..768947ec9 100644 --- a/edg/core/test_port_adapter.py +++ b/edg/core/test_port_adapter.py @@ -6,80 +6,80 @@ class AdapterLink(Link): - def __init__(self) -> None: - super().__init__() - - self.ports = self.Port(Vector(AdapterPort())) + def __init__(self) -> None: + super().__init__() + + self.ports = self.Port(Vector(AdapterPort())) class AdapterPortAdapter(PortAdapter[TestPortSource]): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.src = self.Port(AdapterPort()) - self.dst = self.Port(TestPortSource()) + self.src = self.Port(AdapterPort()) + self.dst = self.Port(TestPortSource()) class AdapterPort(Port[AdapterLink]): - link_type = AdapterLink + link_type = AdapterLink - def as_test_src(self) -> TestPortSource: - return self._convert(AdapterPortAdapter()) + def as_test_src(self) -> TestPortSource: + return self._convert(AdapterPortAdapter()) class AdapterBlock(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(AdapterPort()) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(AdapterPort()) class AdapterTestTop(Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.adapter_src = self.Block(AdapterBlock()) - self.sink = self.Block(TestBlockSink()) - self.test_net = self.connect(self.adapter_src.port.as_test_src(), self.sink.sink) + self.adapter_src = self.Block(AdapterBlock()) + self.sink = self.Block(TestBlockSink()) + self.test_net = self.connect(self.adapter_src.port.as_test_src(), self.sink.sink) class ImplicitConnectTestCase(unittest.TestCase): - def test_connectivity(self) -> None: - pb = AdapterTestTop()._elaborated_def_to_proto() - adapter_pb = edgir.pair_get(pb.blocks, '(adapter)adapter_src.port') - - self.assertEqual(adapter_pb.lib_elem.base.target.name, "edg.core.test_port_adapter.AdapterPortAdapter") - # ignore the other blocks - - self.assertEqual(len(pb.constraints), 4) - constraints = list(map(lambda pair: pair.value, pb.constraints)) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = '(adapter)adapter_src.port' - expected_conn.connected.block_port.ref.steps.add().name = 'dst' - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'source' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'test_net' - expected_conn.connected.link_port.ref.steps.add().name = 'sinks' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - expected_conn.connected.block_port.ref.steps.add().name = 'sink' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.block_port.ref.steps.add().name = 'adapter_src' - expected_conn.connected.block_port.ref.steps.add().name = 'port' - expected_conn.connected.link_port.ref.steps.add().name = '_adapter_src_port_link' # anonymous - expected_conn.connected.link_port.ref.steps.add().name = 'ports' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) - - expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = '_adapter_src_port_link' # anonymous - expected_conn.connected.link_port.ref.steps.add().name = 'ports' - expected_conn.connected.block_port.ref.steps.add().name = '(adapter)adapter_src.port' - expected_conn.connected.block_port.ref.steps.add().name = 'src' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - self.assertIn(expected_conn, constraints) + def test_connectivity(self) -> None: + pb = AdapterTestTop()._elaborated_def_to_proto() + adapter_pb = edgir.pair_get(pb.blocks, "(adapter)adapter_src.port") + + self.assertEqual(adapter_pb.lib_elem.base.target.name, "edg.core.test_port_adapter.AdapterPortAdapter") + # ignore the other blocks + + self.assertEqual(len(pb.constraints), 4) + constraints = list(map(lambda pair: pair.value, pb.constraints)) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "(adapter)adapter_src.port" + expected_conn.connected.block_port.ref.steps.add().name = "dst" + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "source" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "test_net" + expected_conn.connected.link_port.ref.steps.add().name = "sinks" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + expected_conn.connected.block_port.ref.steps.add().name = "sink" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.block_port.ref.steps.add().name = "adapter_src" + expected_conn.connected.block_port.ref.steps.add().name = "port" + expected_conn.connected.link_port.ref.steps.add().name = "_adapter_src_port_link" # anonymous + expected_conn.connected.link_port.ref.steps.add().name = "ports" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) + + expected_conn = edgir.ValueExpr() + expected_conn.connected.link_port.ref.steps.add().name = "_adapter_src_port_link" # anonymous + expected_conn.connected.link_port.ref.steps.add().name = "ports" + expected_conn.connected.block_port.ref.steps.add().name = "(adapter)adapter_src.port" + expected_conn.connected.block_port.ref.steps.add().name = "src" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + self.assertIn(expected_conn, constraints) diff --git a/edg/core/test_port_bridge.py b/edg/core/test_port_bridge.py index 44b9c5a3d..2bbf96cda 100644 --- a/edg/core/test_port_bridge.py +++ b/edg/core/test_port_bridge.py @@ -8,45 +8,45 @@ class PortBridgeProtoTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = TestPortBridge()._elaborated_def_to_proto() - - def test_contains_param(self) -> None: - self.assertEqual(self.pb.ports[0].name, 'outer_port') - self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") - self.assertEqual(self.pb.ports[1].name, 'inner_link') - self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSource") - - def test_constraints(self) -> None: - self.assertEqual(len(self.pb.constraints), 2) - constraints = list(map(lambda pair: pair.value, self.pb.constraints)) - - expected_constr = edgir.ValueExpr() - expected_constr.assign.dst.steps.add().name = 'outer_port' - expected_constr.assign.dst.steps.add().name = 'float_param' - expected_constr.assign.src.ref.steps.add().name = 'inner_link' - expected_constr.assign.src.ref.steps.add().reserved_param = edgir.CONNECTED_LINK - expected_constr.assign.src.ref.steps.add().name = 'float_param_sink_sum' - self.assertIn(expected_constr, constraints) - - expected_constr = edgir.ValueExpr() - expected_constr.binary.op = edgir.BinaryExpr.EQ - expected_constr.assign.dst.steps.add().name = 'outer_port' - expected_constr.assign.dst.steps.add().name = 'range_limit' - expected_constr.assign.src.ref.steps.add().name = 'inner_link' - expected_constr.assign.src.ref.steps.add().reserved_param = edgir.CONNECTED_LINK - expected_constr.assign.src.ref.steps.add().name = 'range_param_sink_common' - self.assertIn(expected_constr, constraints) + @override + def setUp(self) -> None: + self.pb = TestPortBridge()._elaborated_def_to_proto() + + def test_contains_param(self) -> None: + self.assertEqual(self.pb.ports[0].name, "outer_port") + self.assertEqual(self.pb.ports[0].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSink") + self.assertEqual(self.pb.ports[1].name, "inner_link") + self.assertEqual(self.pb.ports[1].value.lib_elem.target.name, "edg.core.test_elaboration_common.TestPortSource") + + def test_constraints(self) -> None: + self.assertEqual(len(self.pb.constraints), 2) + constraints = list(map(lambda pair: pair.value, self.pb.constraints)) + + expected_constr = edgir.ValueExpr() + expected_constr.assign.dst.steps.add().name = "outer_port" + expected_constr.assign.dst.steps.add().name = "float_param" + expected_constr.assign.src.ref.steps.add().name = "inner_link" + expected_constr.assign.src.ref.steps.add().reserved_param = edgir.CONNECTED_LINK + expected_constr.assign.src.ref.steps.add().name = "float_param_sink_sum" + self.assertIn(expected_constr, constraints) + + expected_constr = edgir.ValueExpr() + expected_constr.binary.op = edgir.BinaryExpr.EQ + expected_constr.assign.dst.steps.add().name = "outer_port" + expected_constr.assign.dst.steps.add().name = "range_limit" + expected_constr.assign.src.ref.steps.add().name = "inner_link" + expected_constr.assign.src.ref.steps.add().reserved_param = edgir.CONNECTED_LINK + expected_constr.assign.src.ref.steps.add().name = "range_param_sink_common" + self.assertIn(expected_constr, constraints) class BadPortBridge(PortBridge): - def __init__(self) -> None: - super().__init__() - self.bad_port = self.Port(TestPortSink()) + def __init__(self) -> None: + super().__init__() + self.bad_port = self.Port(TestPortSink()) class PortBridgeBadPortsTestCase(unittest.TestCase): - def test_bad_ports(self) -> None: - with self.assertRaises(AssertionError): - BadPortBridge()._elaborated_def_to_proto() + def test_bad_ports(self) -> None: + with self.assertRaises(AssertionError): + BadPortBridge()._elaborated_def_to_proto() diff --git a/edg/core/test_range.py b/edg/core/test_range.py index 62d622b1a..776389459 100644 --- a/edg/core/test_range.py +++ b/edg/core/test_range.py @@ -3,98 +3,95 @@ from . import Range + class RangeTestCase(unittest.TestCase): - def test_range_contains(self) -> None: - self.assertTrue(0.5 in Range(0, 1)) - self.assertTrue(Range(0, 1) in Range(0, 1)) - - self.assertFalse(Range(0, 1.1) in Range(0, 1)) - - def test_range_tolerance(self) -> None: - self.assertEqual(Range.from_tolerance(100, 0.05).upper, 105) - self.assertEqual(Range.from_tolerance(100, 0.05).lower, 95) - self.assertEqual(Range.from_tolerance(1000, (-0.1, 0.05)).lower, 900) - self.assertEqual(Range.from_tolerance(1000, (-0.1, 0.05)).upper, 1050) - - def test_from(self) -> None: - self.assertEqual(Range.from_lower(500).upper, float('inf')) - self.assertEqual(Range.from_lower(500).lower, 500) - self.assertTrue(500 in Range.from_lower(500)) - self.assertTrue(1e12 in Range.from_lower(500)) - self.assertFalse(499 in Range.from_lower(500)) - self.assertFalse(0 in Range.from_lower(500)) - self.assertFalse(float('-inf') in Range.from_lower(500)) - - self.assertEqual(Range.from_upper(500).upper, 500) - self.assertEqual(Range.from_upper(500).lower, float('-inf')) - self.assertTrue(0 in Range.from_upper(500)) - self.assertTrue(-1e12 in Range.from_upper(500)) - self.assertFalse(1000 in Range.from_upper(500)) - self.assertFalse(float('inf') in Range.from_upper(500)) - - def test_all(self) -> None: - self.assertTrue(Range.all() in Range.all()) - self.assertTrue(0 in Range.all()) - self.assertTrue(1e12 in Range.all()) - - def test_ops(self) -> None: - self.assertTrue(Range(1.1, 5) == Range(1.1, 5)) - self.assertTrue(Range(1.1, 5) != Range(1.2, 5)) - self.assertTrue(Range.all() == Range.all()) - self.assertTrue(Range.all() != Range(1.2, 5)) - - self.assertEqual(Range(1.1, 5) * 2, Range(2.2, 10)) - - self.assertEqual(Range(1, 5).center(), 3) - - def test_intersects(self) -> None: - self.assertTrue(Range(-1, 2).intersects(Range(2, 3))) - self.assertTrue(Range(-1, 2).intersects(Range(0, 3))) - self.assertTrue(Range(-1, 2).intersects(Range(-2, -1))) - self.assertTrue(Range(-1, 2).intersects(Range(-2, 0))) - self.assertTrue(Range(-1, 2).intersects(Range(0, 1))) - self.assertFalse(Range(-1, 2).intersects(Range(3, 4))) - self.assertFalse(Range(-1, 2).intersects(Range(-3, -2))) - - def test_intersect(self) -> None: - self.assertEqual(Range(-1, 2).intersect(Range(2, 3)), Range(2, 2)) - self.assertEqual(Range(-1, 2).intersect(Range(0, 3)), Range(0, 2)) - self.assertEqual(Range(-1, 2).intersect(Range(-2, -1)), Range(-1, -1)) - self.assertEqual(Range(-1, 2).intersect(Range(-2, 0)), Range(-1, 0)) - self.assertEqual(Range(-1, 2).intersect(Range(0, 1)), Range(0, 1)) - with self.assertRaises(ValueError): - Range(-1, 2).intersect(Range(3, 4)) - - def test_hull(self) -> None: - self.assertEqual(Range(-1, 2).hull(Range(2, 3)), Range(-1, 3)) - self.assertEqual(Range(-1, 2).hull(Range(0, 3)), Range(-1, 3)) - self.assertEqual(Range(-1, 2).hull(Range(-2, -1)), Range(-2, 2)) - self.assertEqual(Range(-1, 2).hull(Range(-2, 0)), Range(-2, 2)) - self.assertEqual(Range(-1, 2).hull(Range(0, 1)), Range(-1, 2)) - - def test_shrink_property(self) -> None: - range1 = Range(10, 20) - self.assertEqual(range1.shrink_multiply(1/range1), Range(1, 1)) - - def test_frequency(self) -> None: - """Tests (back-)calculating C from target w and R - so tolerancing flows from R and C to w - instead of w and R to C.""" - R = Range(90, 110) - C = Range.from_tolerance(1e-6, 0.05) - w = 1 / (2 * math.pi * R * C) - solved = (1/(2 * math.pi * w)).shrink_multiply(1/R) - self.assertTrue(math.isclose(C.lower, solved.lower)) - self.assertTrue(math.isclose(C.upper, solved.upper)) - - def test_bound(self) -> None: - """Tests (back-)calculating C from target w and R - so tolerancing flows from R and C to w - instead of w and R to C.""" - self.assertEqual(Range(0, 1).bound_to(Range(0.5, 1.5)), - Range(0.5, 1)) - self.assertEqual(Range(0, 1).bound_to(Range(-0.5, 0.5)), - Range(0, 0.5)) - - self.assertEqual(Range(0, 1).bound_to(Range(10, 20)), - Range(10, 10)) - self.assertEqual(Range(0, 1).bound_to(Range(-20, -10)), - Range(-10, -10)) + def test_range_contains(self) -> None: + self.assertTrue(0.5 in Range(0, 1)) + self.assertTrue(Range(0, 1) in Range(0, 1)) + + self.assertFalse(Range(0, 1.1) in Range(0, 1)) + + def test_range_tolerance(self) -> None: + self.assertEqual(Range.from_tolerance(100, 0.05).upper, 105) + self.assertEqual(Range.from_tolerance(100, 0.05).lower, 95) + self.assertEqual(Range.from_tolerance(1000, (-0.1, 0.05)).lower, 900) + self.assertEqual(Range.from_tolerance(1000, (-0.1, 0.05)).upper, 1050) + + def test_from(self) -> None: + self.assertEqual(Range.from_lower(500).upper, float("inf")) + self.assertEqual(Range.from_lower(500).lower, 500) + self.assertTrue(500 in Range.from_lower(500)) + self.assertTrue(1e12 in Range.from_lower(500)) + self.assertFalse(499 in Range.from_lower(500)) + self.assertFalse(0 in Range.from_lower(500)) + self.assertFalse(float("-inf") in Range.from_lower(500)) + + self.assertEqual(Range.from_upper(500).upper, 500) + self.assertEqual(Range.from_upper(500).lower, float("-inf")) + self.assertTrue(0 in Range.from_upper(500)) + self.assertTrue(-1e12 in Range.from_upper(500)) + self.assertFalse(1000 in Range.from_upper(500)) + self.assertFalse(float("inf") in Range.from_upper(500)) + + def test_all(self) -> None: + self.assertTrue(Range.all() in Range.all()) + self.assertTrue(0 in Range.all()) + self.assertTrue(1e12 in Range.all()) + + def test_ops(self) -> None: + self.assertTrue(Range(1.1, 5) == Range(1.1, 5)) + self.assertTrue(Range(1.1, 5) != Range(1.2, 5)) + self.assertTrue(Range.all() == Range.all()) + self.assertTrue(Range.all() != Range(1.2, 5)) + + self.assertEqual(Range(1.1, 5) * 2, Range(2.2, 10)) + + self.assertEqual(Range(1, 5).center(), 3) + + def test_intersects(self) -> None: + self.assertTrue(Range(-1, 2).intersects(Range(2, 3))) + self.assertTrue(Range(-1, 2).intersects(Range(0, 3))) + self.assertTrue(Range(-1, 2).intersects(Range(-2, -1))) + self.assertTrue(Range(-1, 2).intersects(Range(-2, 0))) + self.assertTrue(Range(-1, 2).intersects(Range(0, 1))) + self.assertFalse(Range(-1, 2).intersects(Range(3, 4))) + self.assertFalse(Range(-1, 2).intersects(Range(-3, -2))) + + def test_intersect(self) -> None: + self.assertEqual(Range(-1, 2).intersect(Range(2, 3)), Range(2, 2)) + self.assertEqual(Range(-1, 2).intersect(Range(0, 3)), Range(0, 2)) + self.assertEqual(Range(-1, 2).intersect(Range(-2, -1)), Range(-1, -1)) + self.assertEqual(Range(-1, 2).intersect(Range(-2, 0)), Range(-1, 0)) + self.assertEqual(Range(-1, 2).intersect(Range(0, 1)), Range(0, 1)) + with self.assertRaises(ValueError): + Range(-1, 2).intersect(Range(3, 4)) + + def test_hull(self) -> None: + self.assertEqual(Range(-1, 2).hull(Range(2, 3)), Range(-1, 3)) + self.assertEqual(Range(-1, 2).hull(Range(0, 3)), Range(-1, 3)) + self.assertEqual(Range(-1, 2).hull(Range(-2, -1)), Range(-2, 2)) + self.assertEqual(Range(-1, 2).hull(Range(-2, 0)), Range(-2, 2)) + self.assertEqual(Range(-1, 2).hull(Range(0, 1)), Range(-1, 2)) + + def test_shrink_property(self) -> None: + range1 = Range(10, 20) + self.assertEqual(range1.shrink_multiply(1 / range1), Range(1, 1)) + + def test_frequency(self) -> None: + """Tests (back-)calculating C from target w and R - so tolerancing flows from R and C to w + instead of w and R to C.""" + R = Range(90, 110) + C = Range.from_tolerance(1e-6, 0.05) + w = 1 / (2 * math.pi * R * C) + solved = (1 / (2 * math.pi * w)).shrink_multiply(1 / R) + self.assertTrue(math.isclose(C.lower, solved.lower)) + self.assertTrue(math.isclose(C.upper, solved.upper)) + + def test_bound(self) -> None: + """Tests (back-)calculating C from target w and R - so tolerancing flows from R and C to w + instead of w and R to C.""" + self.assertEqual(Range(0, 1).bound_to(Range(0.5, 1.5)), Range(0.5, 1)) + self.assertEqual(Range(0, 1).bound_to(Range(-0.5, 0.5)), Range(0, 0.5)) + + self.assertEqual(Range(0, 1).bound_to(Range(10, 20)), Range(10, 10)) + self.assertEqual(Range(0, 1).bound_to(Range(-20, -10)), Range(-10, -10)) diff --git a/edg/core/test_simple_const_prop.py b/edg/core/test_simple_const_prop.py index c4103517c..0c6cf2eaa 100644 --- a/edg/core/test_simple_const_prop.py +++ b/edg/core/test_simple_const_prop.py @@ -7,194 +7,194 @@ class TestConstPropInternal(Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.float_param = self.Parameter(FloatExpr()) - self.range_param = self.Parameter(RangeExpr()) + self.float_param = self.Parameter(FloatExpr()) + self.range_param = self.Parameter(RangeExpr()) class TestParameterConstProp(Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.float_const = self.Parameter(FloatExpr()) - self.float_param = self.Parameter(FloatExpr()) + self.float_const = self.Parameter(FloatExpr()) + self.float_param = self.Parameter(FloatExpr()) - self.range_const = self.Parameter(RangeExpr()) - self.range_param = self.Parameter(RangeExpr()) + self.range_const = self.Parameter(RangeExpr()) + self.range_param = self.Parameter(RangeExpr()) - @override - def contents(self) -> None: - self.assign(self.float_const, 2.0) - self.assign(self.float_param, self.float_const) + @override + def contents(self) -> None: + self.assign(self.float_const, 2.0) + self.assign(self.float_param, self.float_const) - self.assign(self.range_const, Range(1.0, 42.0)) - self.assign(self.range_param, self.range_const) + self.assign(self.range_const, Range(1.0, 42.0)) + self.assign(self.range_param, self.range_const) - self.block = self.Block(TestConstPropInternal()) - self.assign(self.block.float_param, self.float_param) - self.assign(self.block.range_param, self.range_param) + self.block = self.Block(TestConstPropInternal()) + self.assign(self.block.float_param, self.float_param) + self.assign(self.block.range_param, self.range_param) class ConstPropTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.compiled = ScalaCompiler.compile(TestParameterConstProp) + @override + def setUp(self) -> None: + self.compiled = ScalaCompiler.compile(TestParameterConstProp) - def test_float_prop(self) -> None: - self.assertEqual(self.compiled.get_value(['float_const']), 2.0) - self.assertEqual(self.compiled.get_value(['block', 'float_param']), 2.0) + def test_float_prop(self) -> None: + self.assertEqual(self.compiled.get_value(["float_const"]), 2.0) + self.assertEqual(self.compiled.get_value(["block", "float_param"]), 2.0) - def test_range_prop(self) -> None: - self.assertEqual(self.compiled.get_value(['range_const']), Range(1.0, 42.0)) - self.assertEqual(self.compiled.get_value(['block', 'range_param']), Range(1.0, 42.0)) + def test_range_prop(self) -> None: + self.assertEqual(self.compiled.get_value(["range_const"]), Range(1.0, 42.0)) + self.assertEqual(self.compiled.get_value(["block", "range_param"]), Range(1.0, 42.0)) class TestPortConstPropLink(Link): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.a = self.Port(TestPortConstPropPort()) - self.b = self.Port(TestPortConstPropPort()) + self.a = self.Port(TestPortConstPropPort()) + self.b = self.Port(TestPortConstPropPort()) - self.a_float_param = self.Parameter(FloatExpr()) - self.b_float_param = self.Parameter(FloatExpr()) + self.a_float_param = self.Parameter(FloatExpr()) + self.b_float_param = self.Parameter(FloatExpr()) - self.assign(self.a_float_param, self.a.float_param) - self.assign(self.b_float_param, self.b.float_param) + self.assign(self.a_float_param, self.a.float_param) + self.assign(self.b_float_param, self.b.float_param) class TestPortConstPropPort(Port[TestPortConstPropLink]): - link_type = TestPortConstPropLink + link_type = TestPortConstPropLink - def __init__(self) -> None: - super().__init__() - self.float_param = self.Parameter(FloatExpr()) + def __init__(self) -> None: + super().__init__() + self.float_param = self.Parameter(FloatExpr()) class TestPortConstPropInnerBlock(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(TestPortConstPropPort(), optional=True) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(TestPortConstPropPort(), optional=True) class TestPortConstPropOuterBlock(Block): - def __init__(self) -> None: - super().__init__() - self.inner = self.Block(TestPortConstPropInnerBlock()) - self.port = self.Port(TestPortConstPropPort()) - self.connect(self.inner.port, self.port) + def __init__(self) -> None: + super().__init__() + self.inner = self.Block(TestPortConstPropInnerBlock()) + self.port = self.Port(TestPortConstPropPort()) + self.connect(self.inner.port, self.port) class TestPortConstPropTopBlock(Block): - def __init__(self) -> None: - super().__init__() - self.block1 = self.Block(TestPortConstPropInnerBlock()) - self.block2 = self.Block(TestPortConstPropOuterBlock()) # dummy, just to infer a connection - self.link = self.connect(self.block1.port, self.block2.port) - self.assign(self.block1.port.float_param, 3.5) + def __init__(self) -> None: + super().__init__() + self.block1 = self.Block(TestPortConstPropInnerBlock()) + self.block2 = self.Block(TestPortConstPropOuterBlock()) # dummy, just to infer a connection + self.link = self.connect(self.block1.port, self.block2.port) + self.assign(self.block1.port.float_param, 3.5) class ConstPropPortTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.compiled = ScalaCompiler.compile(TestPortConstPropTopBlock) + @override + def setUp(self) -> None: + self.compiled = ScalaCompiler.compile(TestPortConstPropTopBlock) - def test_port_param_prop(self) -> None: - self.assertEqual(self.compiled.get_value(['block1', 'port', 'float_param']), 3.5) - self.assertEqual(self.compiled.get_value(['link', 'a', 'float_param']), 3.5) - self.assertEqual(self.compiled.get_value(['link', 'a_float_param']), 3.5) + def test_port_param_prop(self) -> None: + self.assertEqual(self.compiled.get_value(["block1", "port", "float_param"]), 3.5) + self.assertEqual(self.compiled.get_value(["link", "a", "float_param"]), 3.5) + self.assertEqual(self.compiled.get_value(["link", "a_float_param"]), 3.5) - def test_connected_link(self) -> None: - self.assertEqual(self.compiled.get_value(['block1', 'port', edgir.IS_CONNECTED]), True) - self.assertEqual(self.compiled.get_value(['block2', 'port', edgir.IS_CONNECTED]), True) - self.assertEqual(self.compiled.get_value(['block2', 'inner', 'port', edgir.IS_CONNECTED]), True) + def test_connected_link(self) -> None: + self.assertEqual(self.compiled.get_value(["block1", "port", edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["block2", "port", edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["block2", "inner", "port", edgir.IS_CONNECTED]), True) class TestDisconnectedTopBlock(Block): - def __init__(self) -> None: - super().__init__() - self.block1 = self.Block(TestPortConstPropInnerBlock()) - self.assign(self.block1.port.float_param, 3.5) + def __init__(self) -> None: + super().__init__() + self.block1 = self.Block(TestPortConstPropInnerBlock()) + self.assign(self.block1.port.float_param, 3.5) class DisconnectedPortTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.compiled = ScalaCompiler.compile(TestDisconnectedTopBlock) + @override + def setUp(self) -> None: + self.compiled = ScalaCompiler.compile(TestDisconnectedTopBlock) - def test_disconnected_link(self) -> None: - self.assertEqual(self.compiled.get_value(['block1', 'port', edgir.IS_CONNECTED]), False) + def test_disconnected_link(self) -> None: + self.assertEqual(self.compiled.get_value(["block1", "port", edgir.IS_CONNECTED]), False) class TestPortConstPropBundleLink(Link): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.a = self.Port(TestPortConstPropBundle()) - self.b = self.Port(TestPortConstPropBundle()) + self.a = self.Port(TestPortConstPropBundle()) + self.b = self.Port(TestPortConstPropBundle()) - self.elt1_link = self.connect(self.a.elt1, self.b.elt1) - self.elt2_link = self.connect(self.a.elt2, self.b.elt2) + self.elt1_link = self.connect(self.a.elt1, self.b.elt1) + self.elt2_link = self.connect(self.a.elt2, self.b.elt2) class TestPortConstPropBundle(Bundle[TestPortConstPropBundleLink]): - link_type = TestPortConstPropBundleLink + link_type = TestPortConstPropBundleLink - def __init__(self) -> None: - super().__init__() - self.elt1 = self.Port(TestPortConstPropPort()) - self.elt2 = self.Port(TestPortConstPropPort()) + def __init__(self) -> None: + super().__init__() + self.elt1 = self.Port(TestPortConstPropPort()) + self.elt2 = self.Port(TestPortConstPropPort()) class TestPortConstPropBundleInnerBlock(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(TestPortConstPropBundle()) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(TestPortConstPropBundle()) class TestPortConstPropBundleTopBlock(Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - @override - def contents(self) -> None: - self.block1 = self.Block(TestPortConstPropBundleInnerBlock()) - self.block2 = self.Block(TestPortConstPropBundleInnerBlock()) # dummy, just to infer a connection - self.link = self.connect(self.block1.port, self.block2.port) + @override + def contents(self) -> None: + self.block1 = self.Block(TestPortConstPropBundleInnerBlock()) + self.block2 = self.Block(TestPortConstPropBundleInnerBlock()) # dummy, just to infer a connection + self.link = self.connect(self.block1.port, self.block2.port) - self.assign(self.block1.port.elt1.float_param, 3.5) - self.assign(self.block1.port.elt2.float_param, 6.0) + self.assign(self.block1.port.elt1.float_param, 3.5) + self.assign(self.block1.port.elt2.float_param, 6.0) class ConstPropBundleTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.compiled = ScalaCompiler.compile(TestPortConstPropBundleTopBlock) + @override + def setUp(self) -> None: + self.compiled = ScalaCompiler.compile(TestPortConstPropBundleTopBlock) - def test_port_param_prop(self) -> None: - self.assertEqual(self.compiled.get_value(['block1', 'port', 'elt1', 'float_param']), 3.5) - self.assertEqual(self.compiled.get_value(['block1', 'port', 'elt2', 'float_param']), 6.0) + def test_port_param_prop(self) -> None: + self.assertEqual(self.compiled.get_value(["block1", "port", "elt1", "float_param"]), 3.5) + self.assertEqual(self.compiled.get_value(["block1", "port", "elt2", "float_param"]), 6.0) - self.assertEqual(self.compiled.get_value(['link', 'a', 'elt1', 'float_param']), 3.5) - self.assertEqual(self.compiled.get_value(['link', 'a', 'elt2', 'float_param']), 6.0) + self.assertEqual(self.compiled.get_value(["link", "a", "elt1", "float_param"]), 3.5) + self.assertEqual(self.compiled.get_value(["link", "a", "elt2", "float_param"]), 6.0) - self.assertEqual(self.compiled.get_value(['link', 'elt1_link', 'a', 'float_param']), 3.5) - self.assertEqual(self.compiled.get_value(['link', 'elt2_link', 'a', 'float_param']), 6.0) + self.assertEqual(self.compiled.get_value(["link", "elt1_link", "a", "float_param"]), 3.5) + self.assertEqual(self.compiled.get_value(["link", "elt2_link", "a", "float_param"]), 6.0) - self.assertEqual(self.compiled.get_value(['link', 'elt1_link', 'a_float_param']), 3.5) - self.assertEqual(self.compiled.get_value(['link', 'elt2_link', 'a_float_param']), 6.0) + self.assertEqual(self.compiled.get_value(["link", "elt1_link", "a_float_param"]), 3.5) + self.assertEqual(self.compiled.get_value(["link", "elt2_link", "a_float_param"]), 6.0) - def test_connected_link(self) -> None: - self.assertEqual(self.compiled.get_value(['block1', 'port', edgir.IS_CONNECTED]), True) - self.assertEqual(self.compiled.get_value(['block2', 'port', edgir.IS_CONNECTED]), True) - # Note: inner ports IS_CONNECTED is not defined + def test_connected_link(self) -> None: + self.assertEqual(self.compiled.get_value(["block1", "port", edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["block2", "port", edgir.IS_CONNECTED]), True) + # Note: inner ports IS_CONNECTED is not defined - self.assertEqual(self.compiled.get_value(['link', 'a', edgir.IS_CONNECTED]), True) - self.assertEqual(self.compiled.get_value(['link', 'b', edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["link", "a", edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["link", "b", edgir.IS_CONNECTED]), True) - self.assertEqual(self.compiled.get_value(['link', 'elt1_link', 'a', edgir.IS_CONNECTED]), True) - self.assertEqual(self.compiled.get_value(['link', 'elt1_link', 'b', edgir.IS_CONNECTED]), True) - self.assertEqual(self.compiled.get_value(['link', 'elt2_link', 'a', edgir.IS_CONNECTED]), True) - self.assertEqual(self.compiled.get_value(['link', 'elt2_link', 'b', edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["link", "elt1_link", "a", edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["link", "elt1_link", "b", edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["link", "elt2_link", "a", edgir.IS_CONNECTED]), True) + self.assertEqual(self.compiled.get_value(["link", "elt2_link", "b", edgir.IS_CONNECTED]), True) diff --git a/edg/core/test_simple_expr_eval.py b/edg/core/test_simple_expr_eval.py index 61a0a0753..1b68c2df8 100644 --- a/edg/core/test_simple_expr_eval.py +++ b/edg/core/test_simple_expr_eval.py @@ -7,65 +7,67 @@ class TestEvalExprBlock(Block): - def __init__(self) -> None: - super().__init__() - # Test both the initializer form and the assign form - self.sum_float = self.Parameter(FloatExpr(2 * LiteralConstructor(1) + 3 * LiteralConstructor(1))) - self.sum_range = self.Parameter(RangeExpr()) + def __init__(self) -> None: + super().__init__() + # Test both the initializer form and the assign form + self.sum_float = self.Parameter(FloatExpr(2 * LiteralConstructor(1) + 3 * LiteralConstructor(1))) + self.sum_range = self.Parameter(RangeExpr()) - @override - def contents(self) -> None: - self.assign(self.sum_range, (2, 6) * LiteralConstructor(1) + (7, 8) * LiteralConstructor(1)) + @override + def contents(self) -> None: + self.assign(self.sum_range, (2, 6) * LiteralConstructor(1) + (7, 8) * LiteralConstructor(1)) class EvalExprTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.compiled = ScalaCompiler.compile(TestEvalExprBlock) + @override + def setUp(self) -> None: + self.compiled = ScalaCompiler.compile(TestEvalExprBlock) - def test_sum(self) -> None: - self.assertEqual(self.compiled.get_value(['sum_float']), 5.0) - self.assertEqual(self.compiled.get_value(['sum_range']), Range(9.0, 14.0)) + def test_sum(self) -> None: + self.assertEqual(self.compiled.get_value(["sum_float"]), 5.0) + self.assertEqual(self.compiled.get_value(["sum_range"]), Range(9.0, 14.0)) class TestReductionLink(Link): - def __init__(self) -> None: - super().__init__() - self.ports = self.Port(Vector(TestReductionPort()), optional=True) + def __init__(self) -> None: + super().__init__() + self.ports = self.Port(Vector(TestReductionPort()), optional=True) - self.range_sum = self.Parameter(RangeExpr(self.ports.map_extract(lambda p: p.range_param).sum())) - self.range_intersection = self.Parameter(RangeExpr(self.ports.map_extract(lambda p: p.range_param).intersection())) + self.range_sum = self.Parameter(RangeExpr(self.ports.map_extract(lambda p: p.range_param).sum())) + self.range_intersection = self.Parameter( + RangeExpr(self.ports.map_extract(lambda p: p.range_param).intersection()) + ) class TestReductionPort(Port[TestReductionLink]): - link_type = TestReductionLink + link_type = TestReductionLink - def __init__(self, range_param: RangeLike = RangeExpr()) -> None: - super().__init__() - self.range_param = self.Parameter(RangeExpr(range_param)) + def __init__(self, range_param: RangeLike = RangeExpr()) -> None: + super().__init__() + self.range_param = self.Parameter(RangeExpr(range_param)) class TestEvalPortBlock(Block): - def __init__(self, range_param: RangeLike = RangeExpr()) -> None: - super().__init__() - self.port = self.Port(TestReductionPort(range_param)) + def __init__(self, range_param: RangeLike = RangeExpr()) -> None: + super().__init__() + self.port = self.Port(TestReductionPort(range_param)) class TestEvalReductionBlock(Block): - def __init__(self) -> None: - super().__init__() - self.block1 = self.Block(TestEvalPortBlock((0, 100))) - self.block2 = self.Block(TestEvalPortBlock((1, 10))) - self.block3 = self.Block(TestEvalPortBlock((5, 20))) - self.link = self.connect(self.block1.port, self.block2.port, self.block3.port) + def __init__(self) -> None: + super().__init__() + self.block1 = self.Block(TestEvalPortBlock((0, 100))) + self.block2 = self.Block(TestEvalPortBlock((1, 10))) + self.block3 = self.Block(TestEvalPortBlock((5, 20))) + self.link = self.connect(self.block1.port, self.block2.port, self.block3.port) class EvalReductionTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.compiled = ScalaCompiler.compile(TestEvalReductionBlock) - - def test_reduce(self) -> None: - self.assertEqual(self.compiled.get_value(['link', 'range_sum']), Range(6.0, 130.0)) - self.assertEqual(self.compiled.get_value(['link', 'range_intersection']), Range(5.0, 10.0)) - self.assertEqual(self.compiled.get_value(['link', 'ports', edgir.LENGTH]), 3) + @override + def setUp(self) -> None: + self.compiled = ScalaCompiler.compile(TestEvalReductionBlock) + + def test_reduce(self) -> None: + self.assertEqual(self.compiled.get_value(["link", "range_sum"]), Range(6.0, 130.0)) + self.assertEqual(self.compiled.get_value(["link", "range_intersection"]), Range(5.0, 10.0)) + self.assertEqual(self.compiled.get_value(["link", "ports", edgir.LENGTH]), 3) diff --git a/edg/edgir/__init__.py b/edg/edgir/__init__.py index 1187a6b9c..6a7c079d0 100644 --- a/edg/edgir/__init__.py +++ b/edg/edgir/__init__.py @@ -7,19 +7,41 @@ from .name_pb2 import * from .impl_pb2 import * from .ref_pb2 import LibraryPath, LocalPath, LocalStep, CONNECTED_LINK, IS_CONNECTED, LENGTH, ALLOCATED, NAME -from .elem_pb2 import Port, PortArray, PortLike, Bundle, HierarchyBlock, BlockLike, Link, LinkArray, LinkLike, \ - NamedPortLike, NamedValInit, NamedValueExpr, NamedBlockLike, NamedLinkLike +from .elem_pb2 import ( + Port, + PortArray, + PortLike, + Bundle, + HierarchyBlock, + BlockLike, + Link, + LinkArray, + LinkLike, + NamedPortLike, + NamedValInit, + NamedValueExpr, + NamedBlockLike, + NamedLinkLike, +) from .schema_pb2 import Library, Design -from .expr_pb2 import ConnectedExpr, ExportedExpr, ValueExpr, BinaryExpr, \ - BinarySetExpr, UnaryExpr, UnarySetExpr, MapExtractExpr +from .expr_pb2 import ( + ConnectedExpr, + ExportedExpr, + ValueExpr, + BinaryExpr, + BinarySetExpr, + UnaryExpr, + UnarySetExpr, + MapExtractExpr, +) from .lit_pb2 import ValueLit if TYPE_CHECKING: - from .ref_pb2 import Reserved + from .ref_pb2 import Reserved - # Avoid a runtime circular import, these imports are done locally in scope - # TODO this should be a separate util in edg_core - from ..core.Range import Range + # Avoid a runtime circular import, these imports are done locally in scope + # TODO this should be a separate util in edg_core + from ..core.Range import Range PortTypes = Union[Port, PortArray, Bundle] BlockTypes = HierarchyBlock @@ -29,162 +51,167 @@ def resolve_blocklike(block: BlockLike) -> BlockTypes: - if block.HasField('hierarchy'): - return block.hierarchy - else: - raise ValueError(f"bad blocklike {block}") + if block.HasField("hierarchy"): + return block.hierarchy + else: + raise ValueError(f"bad blocklike {block}") def resolve_linklike(link: LinkLike) -> LinkTypes: - if link.HasField('link'): - return link.link - elif link.HasField('array'): - return link.array - else: - raise ValueError(f"bad linklike {link}") + if link.HasField("link"): + return link.link + elif link.HasField("array"): + return link.array + else: + raise ValueError(f"bad linklike {link}") def resolve_portlike(port: PortLike) -> PortTypes: - if port.HasField('port'): - return port.port - elif port.HasField('array'): - return port.array - elif port.HasField('bundle'): - return port.bundle - else: - raise ValueError(f"bad portlike {port}") + if port.HasField("port"): + return port.port + elif port.HasField("array"): + return port.array + elif port.HasField("bundle"): + return port.bundle + else: + raise ValueError(f"bad portlike {port}") -LitLeafTypes = Union[bool, float, 'Range', str] # TODO for Range: fix me, this prevents a circular import +LitLeafTypes = Union[bool, float, "Range", str] # TODO for Range: fix me, this prevents a circular import LitTypes = Union[LitLeafTypes, List[LitLeafTypes]] def valuelit_to_lit(expr: ValueLit) -> LitTypes: - if expr.HasField('boolean'): - return expr.boolean.val - elif expr.HasField('floating'): - return expr.floating.val - elif expr.HasField('integer'): - return expr.integer.val - elif expr.HasField('range') and \ - expr.range.minimum.HasField('floating') and expr.range.maximum.HasField('floating'): - from ..core.Range import Range # TODO fix me, this prevents a circular import - return Range(expr.range.minimum.floating.val, expr.range.maximum.floating.val) - elif expr.HasField('text'): - return expr.text.val - elif expr.HasField('array'): - elts = [valuelit_to_lit(elt) for elt in expr.array.elts] - if None in elts: - raise ValueError(f"bad valuelit array {expr}") + if expr.HasField("boolean"): + return expr.boolean.val + elif expr.HasField("floating"): + return expr.floating.val + elif expr.HasField("integer"): + return expr.integer.val + elif expr.HasField("range") and expr.range.minimum.HasField("floating") and expr.range.maximum.HasField("floating"): + from ..core.Range import Range # TODO fix me, this prevents a circular import + + return Range(expr.range.minimum.floating.val, expr.range.maximum.floating.val) + elif expr.HasField("text"): + return expr.text.val + elif expr.HasField("array"): + elts = [valuelit_to_lit(elt) for elt in expr.array.elts] + if None in elts: + raise ValueError(f"bad valuelit array {expr}") + else: + return cast(List[LitLeafTypes], elts) else: - return cast(List[LitLeafTypes], elts) - else: - raise ValueError(f"bad valuelit {expr}") + raise ValueError(f"bad valuelit {expr}") def lit_to_valuelit(value: LitTypes) -> ValueLit: - from ..core.Range import Range # TODO fix me, this prevents a circular import - pb = ValueLit() - if isinstance(value, bool): - pb.boolean.val = value - elif isinstance(value, int): - pb.integer.val = value - elif isinstance(value, float): - pb.floating.val = value - elif isinstance(value, Range): - pb.range.minimum.floating.val = value.lower - pb.range.maximum.floating.val = value.upper - elif isinstance(value, str): - pb.text.val = value - elif isinstance(value, list): - pb.array.SetInParent() - for elt in value: - pb.array.elts.add().CopyFrom(lit_to_valuelit(elt)) - else: - raise ValueError(f"unknown lit {value}") - return pb + from ..core.Range import Range # TODO fix me, this prevents a circular import + + pb = ValueLit() + if isinstance(value, bool): + pb.boolean.val = value + elif isinstance(value, int): + pb.integer.val = value + elif isinstance(value, float): + pb.floating.val = value + elif isinstance(value, Range): + pb.range.minimum.floating.val = value.lower + pb.range.maximum.floating.val = value.upper + elif isinstance(value, str): + pb.text.val = value + elif isinstance(value, list): + pb.array.SetInParent() + for elt in value: + pb.array.elts.add().CopyFrom(lit_to_valuelit(elt)) + else: + raise ValueError(f"unknown lit {value}") + return pb def lit_to_expr(value: LitTypes) -> ValueExpr: - pb = ValueExpr() - pb.literal.CopyFrom(lit_to_valuelit(value)) - return pb + pb = ValueExpr() + pb.literal.CopyFrom(lit_to_valuelit(value)) + return pb class Allocate: - """Wrapper around the Allocate LocalPath, for internal use""" - def __init__(self, suggested_name: Optional[str] = None): - if suggested_name is None: - self.suggested_name = "" # for now, no suggested name is represented as empty-string - else: - self.suggested_name = suggested_name - - -def localpath_concat(*elts: Union[LocalPath, str, Allocate, 'Reserved.V']) -> LocalPath: # TODO workaround for broken enum typing - result = LocalPath() - for elt in elts: - if isinstance(elt, LocalPath): - for elt_elt in elt.steps: - result.steps.add().CopyFrom(elt_elt) - elif isinstance(elt, str): - result.steps.add().name = elt - elif isinstance(elt, Allocate): - result.steps.add().allocate = elt.suggested_name - elif elt in (CONNECTED_LINK, IS_CONNECTED, LENGTH, ALLOCATED, NAME): - result.steps.add().reserved_param = elt - else: - raise ValueError(f"unknown localpath elt {elt}") - return result + """Wrapper around the Allocate LocalPath, for internal use""" + + def __init__(self, suggested_name: Optional[str] = None): + if suggested_name is None: + self.suggested_name = "" # for now, no suggested name is represented as empty-string + else: + self.suggested_name = suggested_name + + +def localpath_concat( + *elts: Union[LocalPath, str, Allocate, "Reserved.V"] +) -> LocalPath: # TODO workaround for broken enum typing + result = LocalPath() + for elt in elts: + if isinstance(elt, LocalPath): + for elt_elt in elt.steps: + result.steps.add().CopyFrom(elt_elt) + elif isinstance(elt, str): + result.steps.add().name = elt + elif isinstance(elt, Allocate): + result.steps.add().allocate = elt.suggested_name + elif elt in (CONNECTED_LINK, IS_CONNECTED, LENGTH, ALLOCATED, NAME): + result.steps.add().reserved_param = elt + else: + raise ValueError(f"unknown localpath elt {elt}") + return result def libpath(name: str) -> LibraryPath: - pb = LibraryPath() - pb.target.name = name - return pb + pb = LibraryPath() + pb.target.name = name + return pb -def LocalPathList(path: Iterable[Union[str, Allocate, 'Reserved.V']]) -> LocalPath: - pb = LocalPath() - for step in path: - if isinstance(step, str): - pb.steps.add().name = step - elif isinstance(step, Allocate): - pb.steps.add().allocate = step.suggested_name - elif step in (CONNECTED_LINK, IS_CONNECTED, LENGTH): - pb.steps.add().reserved_param = step - return pb +def LocalPathList(path: Iterable[Union[str, Allocate, "Reserved.V"]]) -> LocalPath: + pb = LocalPath() + for step in path: + if isinstance(step, str): + pb.steps.add().name = step + elif isinstance(step, Allocate): + pb.steps.add().allocate = step.suggested_name + elif step in (CONNECTED_LINK, IS_CONNECTED, LENGTH): + pb.steps.add().reserved_param = step + return pb def AssignLit(dst: Iterable[str], src: LitTypes) -> ValueExpr: - pb = ValueExpr() - pb.assign.dst.CopyFrom(LocalPathList(dst)) - pb.assign.src.CopyFrom(lit_to_expr(src)) - return pb + pb = ValueExpr() + pb.assign.dst.CopyFrom(LocalPathList(dst)) + pb.assign.src.CopyFrom(lit_to_expr(src)) + return pb def AssignRef(dst: Iterable[str], src: Iterable[str]) -> ValueExpr: - pb = ValueExpr() - pb.assign.dst.CopyFrom(LocalPathList(dst)) - pb.assign.src.ref.CopyFrom(LocalPathList(src)) - return pb + pb = ValueExpr() + pb.assign.dst.CopyFrom(LocalPathList(dst)) + pb.assign.src.ref.CopyFrom(LocalPathList(src)) + return pb def local_path_to_str_list(path: LocalPath) -> List[str]: - """Convert a LocalPath to a list of its components. Reserved params are presented as strings.""" - def step_to_str(step: LocalStep) -> str: - if step.HasField('name'): - return step.name - elif step.HasField('reserved_param'): - return { - CONNECTED_LINK: '(link)', - IS_CONNECTED: '(is_connected)' - }[step.reserved_param] - else: - raise ValueError(f"unknown step {step}") - return [step_to_str(step) for step in path.steps] + """Convert a LocalPath to a list of its components. Reserved params are presented as strings.""" + + def step_to_str(step: LocalStep) -> str: + if step.HasField("name"): + return step.name + elif step.HasField("reserved_param"): + return {CONNECTED_LINK: "(link)", IS_CONNECTED: "(is_connected)"}[step.reserved_param] + else: + raise ValueError(f"unknown step {step}") + + return [step_to_str(step) for step in path.steps] + def local_path_to_str(path: LocalPath) -> str: - return '.'.join(local_path_to_str_list(path)) + return ".".join(local_path_to_str_list(path)) + @overload def add_pair(pb: RepeatedCompositeFieldContainer[NamedPortLike], name: str) -> PortLike: ... @@ -197,18 +224,20 @@ def add_pair(pb: RepeatedCompositeFieldContainer[NamedValInit], name: str) -> Va @overload def add_pair(pb: RepeatedCompositeFieldContainer[NamedValueExpr], name: str) -> ValueExpr: ... -def add_pair(pb: Union[ - RepeatedCompositeFieldContainer[NamedPortLike], - RepeatedCompositeFieldContainer[NamedBlockLike], - RepeatedCompositeFieldContainer[NamedLinkLike], - RepeatedCompositeFieldContainer[NamedValInit], - RepeatedCompositeFieldContainer[NamedValueExpr] -], name: str) -> Union[ - PortLike, BlockLike, LinkLike, ValInit, ValueExpr -]: - elt = pb.add() - elt.name = name - return elt.value + +def add_pair( + pb: Union[ + RepeatedCompositeFieldContainer[NamedPortLike], + RepeatedCompositeFieldContainer[NamedBlockLike], + RepeatedCompositeFieldContainer[NamedLinkLike], + RepeatedCompositeFieldContainer[NamedValInit], + RepeatedCompositeFieldContainer[NamedValueExpr], + ], + name: str, +) -> Union[PortLike, BlockLike, LinkLike, ValInit, ValueExpr]: + elt = pb.add() + elt.name = name + return elt.value @overload @@ -222,19 +251,21 @@ def pair_get(pb: RepeatedCompositeFieldContainer[NamedValInit], name: str) -> Va @overload def pair_get(pb: RepeatedCompositeFieldContainer[NamedValueExpr], name: str) -> ValueExpr: ... -def pair_get(pb: Union[ - RepeatedCompositeFieldContainer[NamedPortLike], - RepeatedCompositeFieldContainer[NamedBlockLike], - RepeatedCompositeFieldContainer[NamedLinkLike], - RepeatedCompositeFieldContainer[NamedValInit], - RepeatedCompositeFieldContainer[NamedValueExpr] -], name: str) -> Union[ - PortLike, BlockLike, LinkLike, ValInit, ValueExpr -]: - for elt in pb: - if elt.name == name: - return elt.value - raise KeyError(name) + +def pair_get( + pb: Union[ + RepeatedCompositeFieldContainer[NamedPortLike], + RepeatedCompositeFieldContainer[NamedBlockLike], + RepeatedCompositeFieldContainer[NamedLinkLike], + RepeatedCompositeFieldContainer[NamedValInit], + RepeatedCompositeFieldContainer[NamedValueExpr], + ], + name: str, +) -> Union[PortLike, BlockLike, LinkLike, ValInit, ValueExpr]: + for elt in pb: + if elt.name == name: + return elt.value + raise KeyError(name) @overload @@ -248,16 +279,18 @@ def pair_get_opt(pb: RepeatedCompositeFieldContainer[NamedValInit], name: str) - @overload def pair_get_opt(pb: RepeatedCompositeFieldContainer[NamedValueExpr], name: str) -> Optional[ValueExpr]: ... -def pair_get_opt(pb: Union[ - RepeatedCompositeFieldContainer[NamedPortLike], - RepeatedCompositeFieldContainer[NamedBlockLike], - RepeatedCompositeFieldContainer[NamedLinkLike], - RepeatedCompositeFieldContainer[NamedValInit], - RepeatedCompositeFieldContainer[NamedValueExpr] -], name: str) -> Union[ - Optional[PortLike], Optional[BlockLike], Optional[LinkLike], Optional[ValInit], Optional[ValueExpr] -]: - for elt in pb: - if elt.name == name: - return elt.value - return None + +def pair_get_opt( + pb: Union[ + RepeatedCompositeFieldContainer[NamedPortLike], + RepeatedCompositeFieldContainer[NamedBlockLike], + RepeatedCompositeFieldContainer[NamedLinkLike], + RepeatedCompositeFieldContainer[NamedValInit], + RepeatedCompositeFieldContainer[NamedValueExpr], + ], + name: str, +) -> Union[Optional[PortLike], Optional[BlockLike], Optional[LinkLike], Optional[ValInit], Optional[ValueExpr]]: + for elt in pb: + if elt.name == name: + return elt.value + return None diff --git a/edg/edgir/common_pb2.py b/edg/edgir/common_pb2.py index 01c1565e4..a88fe9b8c 100644 --- a/edg/edgir/common_pb2.py +++ b/edg/edgir/common_pb2.py @@ -1,16 +1,20 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12edgir/common.proto\x12\x0cedgir.common"\x95\x03\n\x08Metadata\x12&\n\x07unknown\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12\x0f\n\x05known\x18\x02 \x01(\tH\x00\x121\n\x07members\x18e \x01(\x0b2\x1e.edgir.common.Metadata.MembersH\x01\x12\x13\n\ttext_leaf\x18f \x01(\tH\x01\x12\x12\n\x08bin_leaf\x18g \x01(\x0cH\x01\x125\n\x0esource_locator\x18n \x01(\x0b2\x1b.edgir.common.SourceLocatorH\x01\x12$\n\x05error\x18p \x01(\x0b2\x13.edgir.common.ErrorH\x01\x1a\x86\x01\n\x07Members\x126\n\x04node\x18\n \x03(\x0b2(.edgir.common.Metadata.Members.NodeEntry\x1aC\n\tNodeEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b2\x16.edgir.common.Metadata:\x028\x01B\x06\n\x04typeB\x06\n\x04meta"\xc9\x01\n\rSourceLocator\x12\x14\n\x0cfile_package\x18\x01 \x01(\t\x12\x13\n\x0bline_offset\x18\x02 \x01(\x05\x12\x12\n\ncol_offset\x18\x03 \x01(\x05\x12;\n\x0bsource_type\x18\x04 \x01(\x0e2&.edgir.common.SourceLocator.SourceType"<\n\nSourceType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0e\n\nDEFINITION\x10\x01\x12\x11\n\rINSTANTIATION\x10\x02"X\n\x05Error\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x11\n\ttraceback\x18\x03 \x01(\t\x12+\n\x06source\x18\x02 \x03(\x0b2\x1b.edgir.common.SourceLocator"\x07\n\x05Emptyb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x12edgir/common.proto\x12\x0cedgir.common"\x95\x03\n\x08Metadata\x12&\n\x07unknown\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12\x0f\n\x05known\x18\x02 \x01(\tH\x00\x121\n\x07members\x18e \x01(\x0b2\x1e.edgir.common.Metadata.MembersH\x01\x12\x13\n\ttext_leaf\x18f \x01(\tH\x01\x12\x12\n\x08bin_leaf\x18g \x01(\x0cH\x01\x125\n\x0esource_locator\x18n \x01(\x0b2\x1b.edgir.common.SourceLocatorH\x01\x12$\n\x05error\x18p \x01(\x0b2\x13.edgir.common.ErrorH\x01\x1a\x86\x01\n\x07Members\x126\n\x04node\x18\n \x03(\x0b2(.edgir.common.Metadata.Members.NodeEntry\x1aC\n\tNodeEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b2\x16.edgir.common.Metadata:\x028\x01B\x06\n\x04typeB\x06\n\x04meta"\xc9\x01\n\rSourceLocator\x12\x14\n\x0cfile_package\x18\x01 \x01(\t\x12\x13\n\x0bline_offset\x18\x02 \x01(\x05\x12\x12\n\ncol_offset\x18\x03 \x01(\x05\x12;\n\x0bsource_type\x18\x04 \x01(\x0e2&.edgir.common.SourceLocator.SourceType"<\n\nSourceType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0e\n\nDEFINITION\x10\x01\x12\x11\n\rINSTANTIATION\x10\x02"X\n\x05Error\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x11\n\ttraceback\x18\x03 \x01(\t\x12+\n\x06source\x18\x02 \x03(\x0b2\x1b.edgir.common.SourceLocator"\x07\n\x05Emptyb\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.common_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.common_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _METADATA_MEMBERS_NODEENTRY._options = None - _METADATA_MEMBERS_NODEENTRY._serialized_options = b'8\x01' + _METADATA_MEMBERS_NODEENTRY._serialized_options = b"8\x01" _METADATA._serialized_start = 37 _METADATA._serialized_end = 442 _METADATA_MEMBERS._serialized_start = 292 @@ -24,4 +28,4 @@ _ERROR._serialized_start = 648 _ERROR._serialized_end = 736 _EMPTY._serialized_start = 738 - _EMPTY._serialized_end = 745 \ No newline at end of file + _EMPTY._serialized_end = 745 diff --git a/edg/edgir/common_pb2.pyi b/edg/edgir/common_pb2.pyi index 05902150f..9ee680ab3 100644 --- a/edg/edgir/common_pb2.pyi +++ b/edg/edgir/common_pb2.pyi @@ -7,6 +7,7 @@ Package : edg.common This is where we keep shared types that we reuse and don't have a good place for. """ + import builtins import collections.abc import google.protobuf.descriptor @@ -15,6 +16,7 @@ import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import sys import typing + if sys.version_info >= (3, 10): import typing as typing_extensions else: @@ -24,6 +26,7 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor @typing_extensions.final class Metadata(google.protobuf.message.Message): """* Arbitrary metadata stored in tree form.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final @@ -38,28 +41,18 @@ class Metadata(google.protobuf.message.Message): key: builtins.str @property - def value(self) -> global___Metadata: - ... - - def __init__(self, *, key: builtins.str=..., value: global___Metadata | None=...) -> None: - ... + def value(self) -> global___Metadata: ... + def __init__(self, *, key: builtins.str = ..., value: global___Metadata | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['key', b'key', 'value', b'value']) -> None: - ... NODE_FIELD_NUMBER: builtins.int @property - def node(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Metadata]: - ... - - def __init__(self, *, node: collections.abc.Mapping[builtins.str, global___Metadata] | None=...) -> None: - ... + def node(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Metadata]: ... + def __init__(self, *, node: collections.abc.Mapping[builtins.str, global___Metadata] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["node", b"node"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['node', b'node']) -> None: - ... UNKNOWN_FIELD_NUMBER: builtins.int KNOWN_FIELD_NUMBER: builtins.int MEMBERS_FIELD_NUMBER: builtins.int @@ -69,16 +62,14 @@ class Metadata(google.protobuf.message.Message): ERROR_FIELD_NUMBER: builtins.int @property - def unknown(self) -> global___Empty: - ... + def unknown(self) -> global___Empty: ... known: builtins.str @property - def members(self) -> global___Metadata.Members: - ... + def members(self) -> global___Metadata.Members: ... text_leaf: builtins.str bin_leaf: builtins.bytes - '* I wanted to split binary and text data, since we might\n just want to dump a raw file/image/datasheet in here\n for safekeeping.\n\n Mixing up binary and textual formats is just a recipe\n for trouble.\n ' + "* I wanted to split binary and text data, since we might\n just want to dump a raw file/image/datasheet in here\n for safekeeping.\n\n Mixing up binary and textual formats is just a recipe\n for trouble.\n " @property def source_locator(self) -> global___SourceLocator: @@ -88,25 +79,73 @@ class Metadata(google.protobuf.message.Message): """ @property - def error(self) -> global___Error: - ... - - def __init__(self, *, unknown: global___Empty | None=..., known: builtins.str=..., members: global___Metadata.Members | None=..., text_leaf: builtins.str=..., bin_leaf: builtins.bytes=..., source_locator: global___SourceLocator | None=..., error: global___Error | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['bin_leaf', b'bin_leaf', 'error', b'error', 'known', b'known', 'members', b'members', 'meta', b'meta', 'source_locator', b'source_locator', 'text_leaf', b'text_leaf', 'type', b'type', 'unknown', b'unknown']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['bin_leaf', b'bin_leaf', 'error', b'error', 'known', b'known', 'members', b'members', 'meta', b'meta', 'source_locator', b'source_locator', 'text_leaf', b'text_leaf', 'type', b'type', 'unknown', b'unknown']) -> None: - ... - + def error(self) -> global___Error: ... + def __init__( + self, + *, + unknown: global___Empty | None = ..., + known: builtins.str = ..., + members: global___Metadata.Members | None = ..., + text_leaf: builtins.str = ..., + bin_leaf: builtins.bytes = ..., + source_locator: global___SourceLocator | None = ..., + error: global___Error | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "bin_leaf", + b"bin_leaf", + "error", + b"error", + "known", + b"known", + "members", + b"members", + "meta", + b"meta", + "source_locator", + b"source_locator", + "text_leaf", + b"text_leaf", + "type", + b"type", + "unknown", + b"unknown", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "bin_leaf", + b"bin_leaf", + "error", + b"error", + "known", + b"known", + "members", + b"members", + "meta", + b"meta", + "source_locator", + b"source_locator", + "text_leaf", + b"text_leaf", + "type", + b"type", + "unknown", + b"unknown", + ], + ) -> None: ... @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal['meta', b'meta']) -> typing_extensions.Literal['members', 'text_leaf', 'bin_leaf', 'source_locator', 'error'] | None: - ... - + def WhichOneof( + self, oneof_group: typing_extensions.Literal["meta", b"meta"] + ) -> typing_extensions.Literal["members", "text_leaf", "bin_leaf", "source_locator", "error"] | None: ... @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal['type', b'type']) -> typing_extensions.Literal['unknown', 'known'] | None: - ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["type", b"type"] + ) -> typing_extensions.Literal["unknown", "known"] | None: ... + global___Metadata = Metadata @typing_extensions.final @@ -115,44 +154,63 @@ class SourceLocator(google.protobuf.message.Message): * For locating source data """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor class _SourceType: - ValueType = typing.NewType('ValueType', builtins.int) + ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType - class _SourceTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SourceLocator._SourceType.ValueType], builtins.type): + class _SourceTypeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SourceLocator._SourceType.ValueType], builtins.type + ): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor UNKNOWN: SourceLocator._SourceType.ValueType DEFINITION: SourceLocator._SourceType.ValueType - 'source defining this class, would be present in library' + "source defining this class, would be present in library" INSTANTIATION: SourceLocator._SourceType.ValueType - 'source of instantiation, would be present in design' + "source of instantiation, would be present in design" - class SourceType(_SourceType, metaclass=_SourceTypeEnumTypeWrapper): - ... + class SourceType(_SourceType, metaclass=_SourceTypeEnumTypeWrapper): ... UNKNOWN: SourceLocator.SourceType.ValueType DEFINITION: SourceLocator.SourceType.ValueType - 'source defining this class, would be present in library' + "source defining this class, would be present in library" INSTANTIATION: SourceLocator.SourceType.ValueType - 'source of instantiation, would be present in design' + "source of instantiation, would be present in design" FILE_PACKAGE_FIELD_NUMBER: builtins.int LINE_OFFSET_FIELD_NUMBER: builtins.int COL_OFFSET_FIELD_NUMBER: builtins.int SOURCE_TYPE_FIELD_NUMBER: builtins.int file_package: builtins.str - 'package name (portable, not tied to an absolute path) that locates the file' + "package name (portable, not tied to an absolute path) that locates the file" line_offset: builtins.int - 'line number' + "line number" col_offset: builtins.int - 'character offset within the line' + "character offset within the line" source_type: global___SourceLocator.SourceType.ValueType - def __init__(self, *, file_package: builtins.str=..., line_offset: builtins.int=..., col_offset: builtins.int=..., source_type: global___SourceLocator.SourceType.ValueType=...) -> None: - ... + def __init__( + self, + *, + file_package: builtins.str = ..., + line_offset: builtins.int = ..., + col_offset: builtins.int = ..., + source_type: global___SourceLocator.SourceType.ValueType = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "col_offset", + b"col_offset", + "file_package", + b"file_package", + "line_offset", + b"line_offset", + "source_type", + b"source_type", + ], + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['col_offset', b'col_offset', 'file_package', b'file_package', 'line_offset', b'line_offset', 'source_type', b'source_type']) -> None: - ... global___SourceLocator = SourceLocator @typing_extensions.final @@ -160,31 +218,40 @@ class Error(google.protobuf.message.Message): """* Used to communicate results of analysis / checking passes. Limited to Block and Link objects. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor MESSAGE_FIELD_NUMBER: builtins.int TRACEBACK_FIELD_NUMBER: builtins.int SOURCE_FIELD_NUMBER: builtins.int message: builtins.str - 'free-form error message' + "free-form error message" traceback: builtins.str - 'full traceback TODO: should there be a structured stack trace?' + "full traceback TODO: should there be a structured stack trace?" @property def source(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SourceLocator]: """source locator, eg line of failing constraint""" - def __init__(self, *, message: builtins.str=..., traceback: builtins.str=..., source: collections.abc.Iterable[global___SourceLocator] | None=...) -> None: - ... + def __init__( + self, + *, + message: builtins.str = ..., + traceback: builtins.str = ..., + source: collections.abc.Iterable[global___SourceLocator] | None = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal["message", b"message", "source", b"source", "traceback", b"traceback"], + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['message', b'message', 'source', b'source', 'traceback', b'traceback']) -> None: - ... global___Error = Error @typing_extensions.final class Empty(google.protobuf.message.Message): """* Placeholder until I figure out how to import properly""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor - def __init__(self) -> None: - ... -global___Empty = Empty \ No newline at end of file + def __init__(self) -> None: ... + +global___Empty = Empty diff --git a/edg/edgir/elem_pb2.py b/edg/edgir/elem_pb2.py index 99844df08..840892556 100644 --- a/edg/edgir/elem_pb2.py +++ b/edg/edgir/elem_pb2.py @@ -1,20 +1,25 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import common_pb2 as edgir_dot_common__pb2 from ..edgir import init_pb2 as edgir_dot_init__pb2 from ..edgir import expr_pb2 as edgir_dot_expr__pb2 from ..edgir import ref_pb2 as edgir_dot_ref__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10edgir/elem.proto\x12\nedgir.elem\x1a\x12edgir/common.proto\x1a\x10edgir/init.proto\x1a\x10edgir/expr.proto\x1a\x0fedgir/ref.proto"@\n\x0cNamedValInit\x12\x0c\n\x04name\x18\x01 \x01(\t\x12"\n\x05value\x18\x02 \x01(\x0b2\x13.edgir.init.ValInit"D\n\x0eNamedValueExpr\x12\x0c\n\x04name\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"B\n\rNamedPortLike\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b2\x14.edgir.elem.PortLike"D\n\x0eNamedBlockLike\x12\x0c\n\x04name\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b2\x15.edgir.elem.BlockLike"B\n\rNamedLinkLike\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b2\x14.edgir.elem.LinkLike"\x95\x02\n\x04Port\x12(\n\x06params\x18( \x03(\x0b2\x18.edgir.elem.NamedValInit\x12/\n\x0bconstraints\x18) \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x0csuperclasses\x18\x15 \x03(\x0b2\x16.edgir.ref.LibraryPath\x122\n\x12super_superclasses\x18\x18 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"\xc1\x02\n\x06Bundle\x12(\n\x06params\x18( \x03(\x0b2\x18.edgir.elem.NamedValInit\x12(\n\x05ports\x18) \x03(\x0b2\x19.edgir.elem.NamedPortLike\x12/\n\x0bconstraints\x18* \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x0csuperclasses\x18\x15 \x03(\x0b2\x16.edgir.ref.LibraryPath\x122\n\x12super_superclasses\x18\x18 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"\xca\x01\n\tPortArray\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x05ports\x18\x0e \x01(\x0b2\x1b.edgir.elem.PortArray.PortsH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata\x1a1\n\x05Ports\x12(\n\x05ports\x18( \x03(\x0b2\x19.edgir.elem.NamedPortLikeB\n\n\x08contains"\xd6\x01\n\x08PortLike\x12(\n\tundefined\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12*\n\x08lib_elem\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPathH\x00\x12 \n\x04port\x18\x03 \x01(\x0b2\x10.edgir.elem.PortH\x00\x12&\n\x05array\x18\x04 \x01(\x0b2\x15.edgir.elem.PortArrayH\x00\x12$\n\x06bundle\x18\x06 \x01(\x0b2\x12.edgir.elem.BundleH\x00B\x04\n\x02is"=\n\tParameter\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12\x0c\n\x04unit\x18\x02 \x01(\t"a\n\x18StringDescriptionElement\x12\x0e\n\x04text\x18\x01 \x01(\tH\x00\x12&\n\x05param\x18\x02 \x01(\x0b2\x15.edgir.elem.ParameterH\x00B\r\n\x0bElementType"\xc4\x06\n\x0eHierarchyBlock\x12(\n\x06params\x18( \x03(\x0b2\x18.edgir.elem.NamedValInit\x12E\n\x0eparam_defaults\x18\x0f \x03(\x0b2-.edgir.elem.HierarchyBlock.ParamDefaultsEntry\x12(\n\x05ports\x18) \x03(\x0b2\x19.edgir.elem.NamedPortLike\x12*\n\x06blocks\x18* \x03(\x0b2\x1a.edgir.elem.NamedBlockLike\x12(\n\x05links\x18+ \x03(\x0b2\x19.edgir.elem.NamedLinkLike\x12/\n\x0bconstraints\x18, \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12*\n\nself_class\x18\x17 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x0csuperclasses\x18\x14 \x03(\x0b2\x16.edgir.ref.LibraryPath\x122\n\x12super_superclasses\x18\x18 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12/\n\x0fprerefine_class\x18\x15 \x01(\x0b2\x16.edgir.ref.LibraryPath\x120\n\x10prerefine_mixins\x18\x19 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12(\n\tgenerator\x18\x16 \x01(\x0b2\x15.edgir.elem.Generator\x12\x13\n\x0bis_abstract\x18\x1e \x01(\x08\x122\n\x12default_refinement\x18\x1f \x01(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata\x129\n\x0bdescription\x18\x01 \x03(\x0b2$.edgir.elem.StringDescriptionElement\x1aK\n\x12ParamDefaultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr:\x028\x01":\n\tGenerator\x12-\n\x0frequired_params\x18\x02 \x03(\x0b2\x14.edgir.ref.LocalPath"\\\n\x0cBlockLibrary\x12$\n\x04base\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12&\n\x06mixins\x18\x03 \x03(\x0b2\x16.edgir.ref.LibraryPath"\x9c\x01\n\tBlockLike\x12(\n\tundefined\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12,\n\x08lib_elem\x18\x05 \x01(\x0b2\x18.edgir.elem.BlockLibraryH\x00\x12/\n\thierarchy\x18\x04 \x01(\x0b2\x1a.edgir.elem.HierarchyBlockH\x00B\x06\n\x04type"\xa4\x03\n\x04Link\x12(\n\x06params\x18( \x03(\x0b2\x18.edgir.elem.NamedValInit\x12(\n\x05ports\x18) \x03(\x0b2\x19.edgir.elem.NamedPortLike\x12(\n\x05links\x18+ \x03(\x0b2\x19.edgir.elem.NamedLinkLike\x12/\n\x0bconstraints\x18* \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x0csuperclasses\x18\x15 \x03(\x0b2\x16.edgir.ref.LibraryPath\x122\n\x12super_superclasses\x18\x18 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata\x129\n\x0bdescription\x18\x01 \x03(\x0b2$.edgir.elem.StringDescriptionElement"\xe2\x01\n\tLinkArray\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12(\n\x05ports\x18) \x03(\x0b2\x19.edgir.elem.NamedPortLike\x12/\n\x0bconstraints\x18* \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12(\n\x05links\x18+ \x03(\x0b2\x19.edgir.elem.NamedLinkLike\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"\xb2\x01\n\x08LinkLike\x12(\n\tundefined\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12*\n\x08lib_elem\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPathH\x00\x12 \n\x04link\x18\x03 \x01(\x0b2\x10.edgir.elem.LinkH\x00\x12&\n\x05array\x18\x04 \x01(\x0b2\x15.edgir.elem.LinkArrayH\x00B\x06\n\x04typeb\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x10edgir/elem.proto\x12\nedgir.elem\x1a\x12edgir/common.proto\x1a\x10edgir/init.proto\x1a\x10edgir/expr.proto\x1a\x0fedgir/ref.proto"@\n\x0cNamedValInit\x12\x0c\n\x04name\x18\x01 \x01(\t\x12"\n\x05value\x18\x02 \x01(\x0b2\x13.edgir.init.ValInit"D\n\x0eNamedValueExpr\x12\x0c\n\x04name\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"B\n\rNamedPortLike\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b2\x14.edgir.elem.PortLike"D\n\x0eNamedBlockLike\x12\x0c\n\x04name\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b2\x15.edgir.elem.BlockLike"B\n\rNamedLinkLike\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b2\x14.edgir.elem.LinkLike"\x95\x02\n\x04Port\x12(\n\x06params\x18( \x03(\x0b2\x18.edgir.elem.NamedValInit\x12/\n\x0bconstraints\x18) \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x0csuperclasses\x18\x15 \x03(\x0b2\x16.edgir.ref.LibraryPath\x122\n\x12super_superclasses\x18\x18 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"\xc1\x02\n\x06Bundle\x12(\n\x06params\x18( \x03(\x0b2\x18.edgir.elem.NamedValInit\x12(\n\x05ports\x18) \x03(\x0b2\x19.edgir.elem.NamedPortLike\x12/\n\x0bconstraints\x18* \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x0csuperclasses\x18\x15 \x03(\x0b2\x16.edgir.ref.LibraryPath\x122\n\x12super_superclasses\x18\x18 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"\xca\x01\n\tPortArray\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x05ports\x18\x0e \x01(\x0b2\x1b.edgir.elem.PortArray.PortsH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata\x1a1\n\x05Ports\x12(\n\x05ports\x18( \x03(\x0b2\x19.edgir.elem.NamedPortLikeB\n\n\x08contains"\xd6\x01\n\x08PortLike\x12(\n\tundefined\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12*\n\x08lib_elem\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPathH\x00\x12 \n\x04port\x18\x03 \x01(\x0b2\x10.edgir.elem.PortH\x00\x12&\n\x05array\x18\x04 \x01(\x0b2\x15.edgir.elem.PortArrayH\x00\x12$\n\x06bundle\x18\x06 \x01(\x0b2\x12.edgir.elem.BundleH\x00B\x04\n\x02is"=\n\tParameter\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12\x0c\n\x04unit\x18\x02 \x01(\t"a\n\x18StringDescriptionElement\x12\x0e\n\x04text\x18\x01 \x01(\tH\x00\x12&\n\x05param\x18\x02 \x01(\x0b2\x15.edgir.elem.ParameterH\x00B\r\n\x0bElementType"\xc4\x06\n\x0eHierarchyBlock\x12(\n\x06params\x18( \x03(\x0b2\x18.edgir.elem.NamedValInit\x12E\n\x0eparam_defaults\x18\x0f \x03(\x0b2-.edgir.elem.HierarchyBlock.ParamDefaultsEntry\x12(\n\x05ports\x18) \x03(\x0b2\x19.edgir.elem.NamedPortLike\x12*\n\x06blocks\x18* \x03(\x0b2\x1a.edgir.elem.NamedBlockLike\x12(\n\x05links\x18+ \x03(\x0b2\x19.edgir.elem.NamedLinkLike\x12/\n\x0bconstraints\x18, \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12*\n\nself_class\x18\x17 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x0csuperclasses\x18\x14 \x03(\x0b2\x16.edgir.ref.LibraryPath\x122\n\x12super_superclasses\x18\x18 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12/\n\x0fprerefine_class\x18\x15 \x01(\x0b2\x16.edgir.ref.LibraryPath\x120\n\x10prerefine_mixins\x18\x19 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12(\n\tgenerator\x18\x16 \x01(\x0b2\x15.edgir.elem.Generator\x12\x13\n\x0bis_abstract\x18\x1e \x01(\x08\x122\n\x12default_refinement\x18\x1f \x01(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata\x129\n\x0bdescription\x18\x01 \x03(\x0b2$.edgir.elem.StringDescriptionElement\x1aK\n\x12ParamDefaultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr:\x028\x01":\n\tGenerator\x12-\n\x0frequired_params\x18\x02 \x03(\x0b2\x14.edgir.ref.LocalPath"\\\n\x0cBlockLibrary\x12$\n\x04base\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12&\n\x06mixins\x18\x03 \x03(\x0b2\x16.edgir.ref.LibraryPath"\x9c\x01\n\tBlockLike\x12(\n\tundefined\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12,\n\x08lib_elem\x18\x05 \x01(\x0b2\x18.edgir.elem.BlockLibraryH\x00\x12/\n\thierarchy\x18\x04 \x01(\x0b2\x1a.edgir.elem.HierarchyBlockH\x00B\x06\n\x04type"\xa4\x03\n\x04Link\x12(\n\x06params\x18( \x03(\x0b2\x18.edgir.elem.NamedValInit\x12(\n\x05ports\x18) \x03(\x0b2\x19.edgir.elem.NamedPortLike\x12(\n\x05links\x18+ \x03(\x0b2\x19.edgir.elem.NamedLinkLike\x12/\n\x0bconstraints\x18* \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12,\n\x0csuperclasses\x18\x15 \x03(\x0b2\x16.edgir.ref.LibraryPath\x122\n\x12super_superclasses\x18\x18 \x03(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata\x129\n\x0bdescription\x18\x01 \x03(\x0b2$.edgir.elem.StringDescriptionElement"\xe2\x01\n\tLinkArray\x12*\n\nself_class\x18\x14 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12(\n\x05ports\x18) \x03(\x0b2\x19.edgir.elem.NamedPortLike\x12/\n\x0bconstraints\x18* \x03(\x0b2\x1a.edgir.elem.NamedValueExpr\x12(\n\x05links\x18+ \x03(\x0b2\x19.edgir.elem.NamedLinkLike\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"\xb2\x01\n\x08LinkLike\x12(\n\tundefined\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12*\n\x08lib_elem\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPathH\x00\x12 \n\x04link\x18\x03 \x01(\x0b2\x10.edgir.elem.LinkH\x00\x12&\n\x05array\x18\x04 \x01(\x0b2\x15.edgir.elem.LinkArrayH\x00B\x06\n\x04typeb\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.elem_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.elem_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _HIERARCHYBLOCK_PARAMDEFAULTSENTRY._options = None - _HIERARCHYBLOCK_PARAMDEFAULTSENTRY._serialized_options = b'8\x01' + _HIERARCHYBLOCK_PARAMDEFAULTSENTRY._serialized_options = b"8\x01" _NAMEDVALINIT._serialized_start = 105 _NAMEDVALINIT._serialized_end = 169 _NAMEDVALUEEXPR._serialized_start = 171 @@ -54,4 +59,4 @@ _LINKARRAY._serialized_start = 3211 _LINKARRAY._serialized_end = 3437 _LINKLIKE._serialized_start = 3440 - _LINKLIKE._serialized_end = 3618 \ No newline at end of file + _LINKLIKE._serialized_end = 3618 diff --git a/edg/edgir/elem_pb2.pyi b/edg/edgir/elem_pb2.pyi index ca8f98142..c1a1b0487 100644 --- a/edg/edgir/elem_pb2.pyi +++ b/edg/edgir/elem_pb2.pyi @@ -13,6 +13,7 @@ We don't strictly differentiate between library elements and elements within a design within the protobuf. In general, when there is a library element, we """ + import builtins import collections.abc from .. import edgir @@ -20,6 +21,7 @@ import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import sys + if sys.version_info >= (3, 8): import typing as typing_extensions else: @@ -32,23 +34,18 @@ class NamedValInit(google.protobuf.message.Message): are unordered (whereas we want to preserve ordering to preserve design intent through the compiler), we use a sequence of these pairs. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor NAME_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int name: builtins.str @property - def value(self) -> edgir.init_pb2.ValInit: - ... + def value(self) -> edgir.init_pb2.ValInit: ... + def __init__(self, *, name: builtins.str = ..., value: edgir.init_pb2.ValInit | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "value", b"value"]) -> None: ... - def __init__(self, *, name: builtins.str=..., value: edgir.init_pb2.ValInit | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['name', b'name', 'value', b'value']) -> None: - ... global___NamedValInit = NamedValInit @typing_extensions.final @@ -59,17 +56,11 @@ class NamedValueExpr(google.protobuf.message.Message): name: builtins.str @property - def value(self) -> edgir.expr_pb2.ValueExpr: - ... - - def __init__(self, *, name: builtins.str=..., value: edgir.expr_pb2.ValueExpr | None=...) -> None: - ... + def value(self) -> edgir.expr_pb2.ValueExpr: ... + def __init__(self, *, name: builtins.str = ..., value: edgir.expr_pb2.ValueExpr | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "value", b"value"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['name', b'name', 'value', b'value']) -> None: - ... global___NamedValueExpr = NamedValueExpr @typing_extensions.final @@ -80,17 +71,11 @@ class NamedPortLike(google.protobuf.message.Message): name: builtins.str @property - def value(self) -> global___PortLike: - ... - - def __init__(self, *, name: builtins.str=..., value: global___PortLike | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... + def value(self) -> global___PortLike: ... + def __init__(self, *, name: builtins.str = ..., value: global___PortLike | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "value", b"value"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['name', b'name', 'value', b'value']) -> None: - ... global___NamedPortLike = NamedPortLike @typing_extensions.final @@ -101,17 +86,11 @@ class NamedBlockLike(google.protobuf.message.Message): name: builtins.str @property - def value(self) -> global___BlockLike: - ... - - def __init__(self, *, name: builtins.str=..., value: global___BlockLike | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... + def value(self) -> global___BlockLike: ... + def __init__(self, *, name: builtins.str = ..., value: global___BlockLike | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "value", b"value"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['name', b'name', 'value', b'value']) -> None: - ... global___NamedBlockLike = NamedBlockLike @typing_extensions.final @@ -122,17 +101,11 @@ class NamedLinkLike(google.protobuf.message.Message): name: builtins.str @property - def value(self) -> global___LinkLike: - ... + def value(self) -> global___LinkLike: ... + def __init__(self, *, name: builtins.str = ..., value: global___LinkLike | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "value", b"value"]) -> None: ... - def __init__(self, *, name: builtins.str=..., value: global___LinkLike | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['name', b'name', 'value', b'value']) -> None: - ... global___NamedLinkLike = NamedLinkLike @typing_extensions.final @@ -146,37 +119,60 @@ class Port(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValInit]: - ... - + def params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValInit]: ... @property - def constraints(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: - ... - + def constraints( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: ... @property - def self_class(self) -> edgir.ref_pb2.LibraryPath: - ... - + def self_class(self) -> edgir.ref_pb2.LibraryPath: ... @property - def superclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def superclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """superclasses, may be empty""" @property - def super_superclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def super_superclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """all (recursive) superclasses above superclasses""" @property def meta(self) -> edgir.common_pb2.Metadata: """TODO: this provides type hierarchy data only, inheritance semantics are currently undefined""" - def __init__(self, *, params: collections.abc.Iterable[global___NamedValInit] | None=..., constraints: collections.abc.Iterable[global___NamedValueExpr] | None=..., self_class: edgir.ref_pb2.LibraryPath | None=..., superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., super_superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta', 'self_class', b'self_class']) -> builtins.bool: - ... + def __init__( + self, + *, + params: collections.abc.Iterable[global___NamedValInit] | None = ..., + constraints: collections.abc.Iterable[global___NamedValueExpr] | None = ..., + self_class: edgir.ref_pb2.LibraryPath | None = ..., + superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + super_superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["meta", b"meta", "self_class", b"self_class"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "constraints", + b"constraints", + "meta", + b"meta", + "params", + b"params", + "self_class", + b"self_class", + "super_superclasses", + b"super_superclasses", + "superclasses", + b"superclasses", + ], + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['constraints', b'constraints', 'meta', b'meta', 'params', b'params', 'self_class', b'self_class', 'super_superclasses', b'super_superclasses', 'superclasses', b'superclasses']) -> None: - ... global___Port = Port @typing_extensions.final @@ -191,41 +187,63 @@ class Bundle(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValInit]: - ... - + def params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValInit]: ... @property - def ports(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedPortLike]: - ... - + def ports(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedPortLike]: ... @property - def constraints(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: - ... - + def constraints( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: ... @property - def self_class(self) -> edgir.ref_pb2.LibraryPath: - ... - + def self_class(self) -> edgir.ref_pb2.LibraryPath: ... @property - def superclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def superclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """superclasses, may be empty""" @property - def super_superclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def super_superclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """all (recursive) superclasses above superclasses""" @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, params: collections.abc.Iterable[global___NamedValInit] | None=..., ports: collections.abc.Iterable[global___NamedPortLike] | None=..., constraints: collections.abc.Iterable[global___NamedValueExpr] | None=..., self_class: edgir.ref_pb2.LibraryPath | None=..., superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., super_superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + params: collections.abc.Iterable[global___NamedValInit] | None = ..., + ports: collections.abc.Iterable[global___NamedPortLike] | None = ..., + constraints: collections.abc.Iterable[global___NamedValueExpr] | None = ..., + self_class: edgir.ref_pb2.LibraryPath | None = ..., + superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + super_superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["meta", b"meta", "self_class", b"self_class"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "constraints", + b"constraints", + "meta", + b"meta", + "params", + b"params", + "ports", + b"ports", + "self_class", + b"self_class", + "super_superclasses", + b"super_superclasses", + "superclasses", + b"superclasses", + ], + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta', 'self_class', b'self_class']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['constraints', b'constraints', 'meta', b'meta', 'params', b'params', 'ports', b'ports', 'self_class', b'self_class', 'super_superclasses', b'super_superclasses', 'superclasses', b'superclasses']) -> None: - ... global___Bundle = Bundle @typing_extensions.final @@ -238,14 +256,12 @@ class PortArray(google.protobuf.message.Message): PORTS_FIELD_NUMBER: builtins.int @property - def ports(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedPortLike]: - ... - - def __init__(self, *, ports: collections.abc.Iterable[global___NamedPortLike] | None=...) -> None: - ... + def ports( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedPortLike]: ... + def __init__(self, *, ports: collections.abc.Iterable[global___NamedPortLike] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["ports", b"ports"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['ports', b'ports']) -> None: - ... SELF_CLASS_FIELD_NUMBER: builtins.int PORTS_FIELD_NUMBER: builtins.int META_FIELD_NUMBER: builtins.int @@ -257,29 +273,38 @@ class PortArray(google.protobuf.message.Message): """ @property - def ports(self) -> global___PortArray.Ports: - ... - - @property - def meta(self) -> edgir.common_pb2.Metadata: - ... + def ports(self) -> global___PortArray.Ports: ... + @property + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + self_class: edgir.ref_pb2.LibraryPath | None = ..., + ports: global___PortArray.Ports | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "contains", b"contains", "meta", b"meta", "ports", b"ports", "self_class", b"self_class" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "contains", b"contains", "meta", b"meta", "ports", b"ports", "self_class", b"self_class" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["contains", b"contains"] + ) -> typing_extensions.Literal["ports"] | None: ... - def __init__(self, *, self_class: edgir.ref_pb2.LibraryPath | None=..., ports: global___PortArray.Ports | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['contains', b'contains', 'meta', b'meta', 'ports', b'ports', 'self_class', b'self_class']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['contains', b'contains', 'meta', b'meta', 'ports', b'ports', 'self_class', b'self_class']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['contains', b'contains']) -> typing_extensions.Literal['ports'] | None: - ... global___PortArray = PortArray @typing_extensions.final class PortLike(google.protobuf.message.Message): """* Wrapper for different port like elements""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor UNDEFINED_FIELD_NUMBER: builtins.int LIB_ELEM_FIELD_NUMBER: builtins.int @@ -288,36 +313,66 @@ class PortLike(google.protobuf.message.Message): BUNDLE_FIELD_NUMBER: builtins.int @property - def undefined(self) -> edgir.common_pb2.Empty: - ... - + def undefined(self) -> edgir.common_pb2.Empty: ... @property - def lib_elem(self) -> edgir.ref_pb2.LibraryPath: - ... - + def lib_elem(self) -> edgir.ref_pb2.LibraryPath: ... @property def port(self) -> global___Port: """* 'port' disallowed w/in the library""" @property - def array(self) -> global___PortArray: - ... - + def array(self) -> global___PortArray: ... @property def bundle(self) -> global___Bundle: """* 'bundle' disallowed w/in the library""" - def __init__(self, *, undefined: edgir.common_pb2.Empty | None=..., lib_elem: edgir.ref_pb2.LibraryPath | None=..., port: global___Port | None=..., array: global___PortArray | None=..., bundle: global___Bundle | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['array', b'array', 'bundle', b'bundle', 'is', b'is', 'lib_elem', b'lib_elem', 'port', b'port', 'undefined', b'undefined']) -> builtins.bool: - ... + def __init__( + self, + *, + undefined: edgir.common_pb2.Empty | None = ..., + lib_elem: edgir.ref_pb2.LibraryPath | None = ..., + port: global___Port | None = ..., + array: global___PortArray | None = ..., + bundle: global___Bundle | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "bundle", + b"bundle", + "is", + b"is", + "lib_elem", + b"lib_elem", + "port", + b"port", + "undefined", + b"undefined", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "bundle", + b"bundle", + "is", + b"is", + "lib_elem", + b"lib_elem", + "port", + b"port", + "undefined", + b"undefined", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["is", b"is"] + ) -> typing_extensions.Literal["undefined", "lib_elem", "port", "array", "bundle"] | None: ... - def ClearField(self, field_name: typing_extensions.Literal['array', b'array', 'bundle', b'bundle', 'is', b'is', 'lib_elem', b'lib_elem', 'port', b'port', 'undefined', b'undefined']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['is', b'is']) -> typing_extensions.Literal['undefined', 'lib_elem', 'port', 'array', 'bundle'] | None: - ... global___PortLike = PortLike @typing_extensions.final @@ -327,18 +382,13 @@ class Parameter(google.protobuf.message.Message): UNIT_FIELD_NUMBER: builtins.int @property - def path(self) -> edgir.ref_pb2.LocalPath: - ... + def path(self) -> edgir.ref_pb2.LocalPath: ... unit: builtins.str - def __init__(self, *, path: edgir.ref_pb2.LocalPath | None=..., unit: builtins.str=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['path', b'path']) -> builtins.bool: - ... + def __init__(self, *, path: edgir.ref_pb2.LocalPath | None = ..., unit: builtins.str = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["path", b"path"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["path", b"path", "unit", b"unit"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['path', b'path', 'unit', b'unit']) -> None: - ... global___Parameter = Parameter @typing_extensions.final @@ -349,20 +399,18 @@ class StringDescriptionElement(google.protobuf.message.Message): text: builtins.str @property - def param(self) -> global___Parameter: - ... + def param(self) -> global___Parameter: ... + def __init__(self, *, text: builtins.str = ..., param: global___Parameter | None = ...) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["ElementType", b"ElementType", "param", b"param", "text", b"text"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["ElementType", b"ElementType", "param", b"param", "text", b"text"] + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["ElementType", b"ElementType"] + ) -> typing_extensions.Literal["text", "param"] | None: ... - def __init__(self, *, text: builtins.str=..., param: global___Parameter | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['ElementType', b'ElementType', 'param', b'param', 'text', b'text']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['ElementType', b'ElementType', 'param', b'param', 'text', b'text']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['ElementType', b'ElementType']) -> typing_extensions.Literal['text', 'param'] | None: - ... global___StringDescriptionElement = StringDescriptionElement @typing_extensions.final @@ -377,17 +425,11 @@ class HierarchyBlock(google.protobuf.message.Message): key: builtins.str @property - def value(self) -> edgir.expr_pb2.ValueExpr: - ... + def value(self) -> edgir.expr_pb2.ValueExpr: ... + def __init__(self, *, key: builtins.str = ..., value: edgir.expr_pb2.ValueExpr | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... - def __init__(self, *, key: builtins.str=..., value: edgir.expr_pb2.ValueExpr | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['key', b'key', 'value', b'value']) -> None: - ... PARAMS_FIELD_NUMBER: builtins.int PARAM_DEFAULTS_FIELD_NUMBER: builtins.int PORTS_FIELD_NUMBER: builtins.int @@ -406,9 +448,7 @@ class HierarchyBlock(google.protobuf.message.Message): DESCRIPTION_FIELD_NUMBER: builtins.int @property - def params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValInit]: - ... - + def params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValInit]: ... @property def param_defaults(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, edgir.expr_pb2.ValueExpr]: """Refinements may introduce new parameters which would not be assigned a value in the parent class. @@ -417,9 +457,7 @@ class HierarchyBlock(google.protobuf.message.Message): """ @property - def ports(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedPortLike]: - ... - + def ports(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedPortLike]: ... @property def blocks(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedBlockLike]: """* Bridges, which adapt an edge port to a link port - eg, edge VoltageSink to an internal link @@ -428,11 +466,11 @@ class HierarchyBlock(google.protobuf.message.Message): """ @property - def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedLinkLike]: - ... - + def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedLinkLike]: ... @property - def constraints(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: + def constraints( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: """* Connections between internal block and link ports are represented by connected constraints. Connections between internal; block and edge (of this block) ports are represented by exported constraints. """ @@ -442,11 +480,15 @@ class HierarchyBlock(google.protobuf.message.Message): """self class, equivalent to the library name""" @property - def superclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def superclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """immediate superclasses, may be empty""" @property - def super_superclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def super_superclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """all (recursive) superclasses above superclasses""" @property @@ -454,35 +496,100 @@ class HierarchyBlock(google.protobuf.message.Message): """class pre-refinement, only defined if refined""" @property - def prerefine_mixins(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def prerefine_mixins( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """mixins pre-refinement, from library elem""" @property def generator(self) -> global___Generator: """optional, and removed upon invocation""" is_abstract: builtins.bool - 'true if self_class is abstract, and should error if used in a design' + "true if self_class is abstract, and should error if used in a design" @property def default_refinement(self) -> edgir.ref_pb2.LibraryPath: """optional default refinement subclass, only valid for library blocks""" @property - def meta(self) -> edgir.common_pb2.Metadata: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + @property + def description( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___StringDescriptionElement]: ... + def __init__( + self, + *, + params: collections.abc.Iterable[global___NamedValInit] | None = ..., + param_defaults: collections.abc.Mapping[builtins.str, edgir.expr_pb2.ValueExpr] | None = ..., + ports: collections.abc.Iterable[global___NamedPortLike] | None = ..., + blocks: collections.abc.Iterable[global___NamedBlockLike] | None = ..., + links: collections.abc.Iterable[global___NamedLinkLike] | None = ..., + constraints: collections.abc.Iterable[global___NamedValueExpr] | None = ..., + self_class: edgir.ref_pb2.LibraryPath | None = ..., + superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + super_superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + prerefine_class: edgir.ref_pb2.LibraryPath | None = ..., + prerefine_mixins: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + generator: global___Generator | None = ..., + is_abstract: builtins.bool = ..., + default_refinement: edgir.ref_pb2.LibraryPath | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + description: collections.abc.Iterable[global___StringDescriptionElement] | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "default_refinement", + b"default_refinement", + "generator", + b"generator", + "meta", + b"meta", + "prerefine_class", + b"prerefine_class", + "self_class", + b"self_class", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "blocks", + b"blocks", + "constraints", + b"constraints", + "default_refinement", + b"default_refinement", + "description", + b"description", + "generator", + b"generator", + "is_abstract", + b"is_abstract", + "links", + b"links", + "meta", + b"meta", + "param_defaults", + b"param_defaults", + "params", + b"params", + "ports", + b"ports", + "prerefine_class", + b"prerefine_class", + "prerefine_mixins", + b"prerefine_mixins", + "self_class", + b"self_class", + "super_superclasses", + b"super_superclasses", + "superclasses", + b"superclasses", + ], + ) -> None: ... - @property - def description(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___StringDescriptionElement]: - ... - - def __init__(self, *, params: collections.abc.Iterable[global___NamedValInit] | None=..., param_defaults: collections.abc.Mapping[builtins.str, edgir.expr_pb2.ValueExpr] | None=..., ports: collections.abc.Iterable[global___NamedPortLike] | None=..., blocks: collections.abc.Iterable[global___NamedBlockLike] | None=..., links: collections.abc.Iterable[global___NamedLinkLike] | None=..., constraints: collections.abc.Iterable[global___NamedValueExpr] | None=..., self_class: edgir.ref_pb2.LibraryPath | None=..., superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., super_superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., prerefine_class: edgir.ref_pb2.LibraryPath | None=..., prerefine_mixins: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., generator: global___Generator | None=..., is_abstract: builtins.bool=..., default_refinement: edgir.ref_pb2.LibraryPath | None=..., meta: edgir.common_pb2.Metadata | None=..., description: collections.abc.Iterable[global___StringDescriptionElement] | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['default_refinement', b'default_refinement', 'generator', b'generator', 'meta', b'meta', 'prerefine_class', b'prerefine_class', 'self_class', b'self_class']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['blocks', b'blocks', 'constraints', b'constraints', 'default_refinement', b'default_refinement', 'description', b'description', 'generator', b'generator', 'is_abstract', b'is_abstract', 'links', b'links', 'meta', b'meta', 'param_defaults', b'param_defaults', 'params', b'params', 'ports', b'ports', 'prerefine_class', b'prerefine_class', 'prerefine_mixins', b'prerefine_mixins', 'self_class', b'self_class', 'super_superclasses', b'super_superclasses', 'superclasses', b'superclasses']) -> None: - ... global___HierarchyBlock = HierarchyBlock @typing_extensions.final @@ -491,16 +598,16 @@ class Generator(google.protobuf.message.Message): REQUIRED_PARAMS_FIELD_NUMBER: builtins.int @property - def required_params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LocalPath]: + def required_params( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LocalPath]: """Parameters that must be defined for the generator to fire. These parameters are the only ones accessible to the generator. """ - def __init__(self, *, required_params: collections.abc.Iterable[edgir.ref_pb2.LocalPath] | None=...) -> None: - ... + def __init__(self, *, required_params: collections.abc.Iterable[edgir.ref_pb2.LocalPath] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["required_params", b"required_params"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['required_params', b'required_params']) -> None: - ... global___Generator = Generator @typing_extensions.final @@ -510,21 +617,20 @@ class BlockLibrary(google.protobuf.message.Message): MIXINS_FIELD_NUMBER: builtins.int @property - def base(self) -> edgir.ref_pb2.LibraryPath: - ... - + def base(self) -> edgir.ref_pb2.LibraryPath: ... @property - def mixins(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: - ... - - def __init__(self, *, base: edgir.ref_pb2.LibraryPath | None=..., mixins: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=...) -> None: - ... + def mixins( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: ... + def __init__( + self, + *, + base: edgir.ref_pb2.LibraryPath | None = ..., + mixins: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["base", b"base"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["base", b"base", "mixins", b"mixins"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['base', b'base']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['base', b'base', 'mixins', b'mixins']) -> None: - ... global___BlockLibrary = BlockLibrary @typing_extensions.final @@ -535,28 +641,36 @@ class BlockLike(google.protobuf.message.Message): HIERARCHY_FIELD_NUMBER: builtins.int @property - def undefined(self) -> edgir.common_pb2.Empty: - ... - + def undefined(self) -> edgir.common_pb2.Empty: ... @property - def lib_elem(self) -> global___BlockLibrary: - ... - + def lib_elem(self) -> global___BlockLibrary: ... @property def hierarchy(self) -> global___HierarchyBlock: """* not allowed w/in the library""" - def __init__(self, *, undefined: edgir.common_pb2.Empty | None=..., lib_elem: global___BlockLibrary | None=..., hierarchy: global___HierarchyBlock | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['hierarchy', b'hierarchy', 'lib_elem', b'lib_elem', 'type', b'type', 'undefined', b'undefined']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['hierarchy', b'hierarchy', 'lib_elem', b'lib_elem', 'type', b'type', 'undefined', b'undefined']) -> None: - ... + def __init__( + self, + *, + undefined: edgir.common_pb2.Empty | None = ..., + lib_elem: global___BlockLibrary | None = ..., + hierarchy: global___HierarchyBlock | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "hierarchy", b"hierarchy", "lib_elem", b"lib_elem", "type", b"type", "undefined", b"undefined" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "hierarchy", b"hierarchy", "lib_elem", b"lib_elem", "type", b"type", "undefined", b"undefined" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["type", b"type"] + ) -> typing_extensions.Literal["undefined", "lib_elem", "hierarchy"] | None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal['type', b'type']) -> typing_extensions.Literal['undefined', 'lib_elem', 'hierarchy'] | None: - ... global___BlockLike = BlockLike @typing_extensions.final @@ -573,31 +687,27 @@ class Link(google.protobuf.message.Message): DESCRIPTION_FIELD_NUMBER: builtins.int @property - def params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValInit]: - ... - + def params(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValInit]: ... @property - def ports(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedPortLike]: - ... - + def ports(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedPortLike]: ... @property - def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedLinkLike]: - ... - + def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedLinkLike]: ... @property - def constraints(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: - ... - + def constraints( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: ... @property - def self_class(self) -> edgir.ref_pb2.LibraryPath: - ... - + def self_class(self) -> edgir.ref_pb2.LibraryPath: ... @property - def superclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def superclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """superclasses, may be empty""" @property - def super_superclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: + def super_superclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: """all (recursive) superclasses above superclasses""" @property @@ -605,17 +715,49 @@ class Link(google.protobuf.message.Message): """TODO: this provides type hierarchy data only, inheritance semantics are currently undefined""" @property - def description(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___StringDescriptionElement]: - ... - - def __init__(self, *, params: collections.abc.Iterable[global___NamedValInit] | None=..., ports: collections.abc.Iterable[global___NamedPortLike] | None=..., links: collections.abc.Iterable[global___NamedLinkLike] | None=..., constraints: collections.abc.Iterable[global___NamedValueExpr] | None=..., self_class: edgir.ref_pb2.LibraryPath | None=..., superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., super_superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=..., meta: edgir.common_pb2.Metadata | None=..., description: collections.abc.Iterable[global___StringDescriptionElement] | None=...) -> None: - ... + def description( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___StringDescriptionElement]: ... + def __init__( + self, + *, + params: collections.abc.Iterable[global___NamedValInit] | None = ..., + ports: collections.abc.Iterable[global___NamedPortLike] | None = ..., + links: collections.abc.Iterable[global___NamedLinkLike] | None = ..., + constraints: collections.abc.Iterable[global___NamedValueExpr] | None = ..., + self_class: edgir.ref_pb2.LibraryPath | None = ..., + superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + super_superclasses: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + description: collections.abc.Iterable[global___StringDescriptionElement] | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["meta", b"meta", "self_class", b"self_class"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "constraints", + b"constraints", + "description", + b"description", + "links", + b"links", + "meta", + b"meta", + "params", + b"params", + "ports", + b"ports", + "self_class", + b"self_class", + "super_superclasses", + b"super_superclasses", + "superclasses", + b"superclasses", + ], + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta', 'self_class', b'self_class']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['constraints', b'constraints', 'description', b'description', 'links', b'links', 'meta', b'meta', 'params', b'params', 'ports', b'ports', 'self_class', b'self_class', 'super_superclasses', b'super_superclasses', 'superclasses', b'superclasses']) -> None: - ... global___Link = Link @typing_extensions.final @@ -640,25 +782,43 @@ class LinkArray(google.protobuf.message.Message): """ @property - def constraints(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: + def constraints( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedValueExpr]: """includes all exported constraints to map link ports to my ports""" @property - def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedLinkLike]: - ... - - @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, self_class: edgir.ref_pb2.LibraryPath | None=..., ports: collections.abc.Iterable[global___NamedPortLike] | None=..., constraints: collections.abc.Iterable[global___NamedValueExpr] | None=..., links: collections.abc.Iterable[global___NamedLinkLike] | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... + def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NamedLinkLike]: ... + @property + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + self_class: edgir.ref_pb2.LibraryPath | None = ..., + ports: collections.abc.Iterable[global___NamedPortLike] | None = ..., + constraints: collections.abc.Iterable[global___NamedValueExpr] | None = ..., + links: collections.abc.Iterable[global___NamedLinkLike] | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["meta", b"meta", "self_class", b"self_class"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "constraints", + b"constraints", + "links", + b"links", + "meta", + b"meta", + "ports", + b"ports", + "self_class", + b"self_class", + ], + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta', 'self_class', b'self_class']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['constraints', b'constraints', 'links', b'links', 'meta', b'meta', 'ports', b'ports', 'self_class', b'self_class']) -> None: - ... global___LinkArray = LinkArray @typing_extensions.final @@ -670,30 +830,37 @@ class LinkLike(google.protobuf.message.Message): ARRAY_FIELD_NUMBER: builtins.int @property - def undefined(self) -> edgir.common_pb2.Empty: - ... - + def undefined(self) -> edgir.common_pb2.Empty: ... @property - def lib_elem(self) -> edgir.ref_pb2.LibraryPath: - ... - + def lib_elem(self) -> edgir.ref_pb2.LibraryPath: ... @property def link(self) -> global___Link: """* not allowed w/in the library""" @property - def array(self) -> global___LinkArray: - ... - - def __init__(self, *, undefined: edgir.common_pb2.Empty | None=..., lib_elem: edgir.ref_pb2.LibraryPath | None=..., link: global___Link | None=..., array: global___LinkArray | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['array', b'array', 'lib_elem', b'lib_elem', 'link', b'link', 'type', b'type', 'undefined', b'undefined']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['array', b'array', 'lib_elem', b'lib_elem', 'link', b'link', 'type', b'type', 'undefined', b'undefined']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['type', b'type']) -> typing_extensions.Literal['undefined', 'lib_elem', 'link', 'array'] | None: - ... -global___LinkLike = LinkLike \ No newline at end of file + def array(self) -> global___LinkArray: ... + def __init__( + self, + *, + undefined: edgir.common_pb2.Empty | None = ..., + lib_elem: edgir.ref_pb2.LibraryPath | None = ..., + link: global___Link | None = ..., + array: global___LinkArray | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "array", b"array", "lib_elem", b"lib_elem", "link", b"link", "type", b"type", "undefined", b"undefined" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "array", b"array", "lib_elem", b"lib_elem", "link", b"link", "type", b"type", "undefined", b"undefined" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["type", b"type"] + ) -> typing_extensions.Literal["undefined", "lib_elem", "link", "array"] | None: ... + +global___LinkLike = LinkLike diff --git a/edg/edgir/expr_pb2.py b/edg/edgir/expr_pb2.py index 110e1660f..6b0f7950b 100644 --- a/edg/edgir/expr_pb2.py +++ b/edg/edgir/expr_pb2.py @@ -1,19 +1,24 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import ref_pb2 as edgir_dot_ref__pb2 from ..edgir import common_pb2 as edgir_dot_common__pb2 from ..edgir import lit_pb2 as edgir_dot_lit__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10edgir/expr.proto\x12\nedgir.expr\x1a\x0fedgir/ref.proto\x1a\x12edgir/common.proto\x1a\x0fedgir/lit.proto"\xb4\x01\n\tUnaryExpr\x12$\n\x02op\x18\x01 \x01(\x0e2\x18.edgir.expr.UnaryExpr.Op\x12"\n\x03val\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"]\n\x02Op\x12\r\n\tUNDEFINED\x10\x00\x12\n\n\x06NEGATE\x10\x01\x12\x07\n\x03NOT\x10\x02\x12\n\n\x06INVERT\x10\x03\x12\x07\n\x03MIN\x10\x04\x12\x07\n\x03MAX\x10\x05\x12\n\n\x06CENTER\x10\x06\x12\t\n\x05WIDTH\x10\x07"\x9f\x02\n\x0cUnarySetExpr\x12\'\n\x02op\x18\x01 \x01(\x0e2\x1b.edgir.expr.UnarySetExpr.Op\x12#\n\x04vals\x18\x04 \x01(\x0b2\x15.edgir.expr.ValueExpr"\xc0\x01\n\x02Op\x12\r\n\tUNDEFINED\x10\x00\x12\x07\n\x03SUM\x10\x01\x12\x0c\n\x08ALL_TRUE\x10\x02\x12\x0c\n\x08ANY_TRUE\x10\x03\x12\n\n\x06ALL_EQ\x10\x04\x12\x0e\n\nALL_UNIQUE\x10\x05\x12\x0b\n\x07MAXIMUM\x10\n\x12\x0b\n\x07MINIMUM\x10\x0b\x12\x0f\n\x0bSET_EXTRACT\x10\x0c\x12\x10\n\x0cINTERSECTION\x10\r\x12\x08\n\x04HULL\x10\x0e\x12\n\n\x06NEGATE\x10\x14\x12\n\n\x06INVERT\x10\x15\x12\x0b\n\x07FLATTEN\x10\x1e"\xd4\x02\n\nBinaryExpr\x12%\n\x02op\x18\x01 \x01(\x0e2\x19.edgir.expr.BinaryExpr.Op\x12"\n\x03lhs\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x03rhs\x18\x03 \x01(\x0b2\x15.edgir.expr.ValueExpr"\xd6\x01\n\x02Op\x12\r\n\tUNDEFINED\x10\x00\x12\x07\n\x03ADD\x10\n\x12\x08\n\x04MULT\x10\x0c\x12\x0f\n\x0bSHRINK_MULT\x107\x12\x07\n\x03AND\x10\x14\x12\x06\n\x02OR\x10\x15\x12\x07\n\x03XOR\x10\x16\x12\x0b\n\x07IMPLIES\x10\x17\x12\x06\n\x02EQ\x10\x1e\x12\x07\n\x03NEQ\x10\x1f\x12\x06\n\x02GT\x10(\x12\x07\n\x03GTE\x10)\x12\x06\n\x02LT\x10*\x12\x07\n\x03LTE\x10,\x12\x07\n\x03MAX\x10-\x12\x07\n\x03MIN\x10.\x12\x10\n\x0cINTERSECTION\x103\x12\x08\n\x04HULL\x106\x12\n\n\x06WITHIN\x105\x12\t\n\x05RANGE\x10\x01"\xb7\x01\n\rBinarySetExpr\x12(\n\x02op\x18\x01 \x01(\x0e2\x1c.edgir.expr.BinarySetExpr.Op\x12$\n\x05lhset\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x03rhs\x18\x03 \x01(\x0b2\x15.edgir.expr.ValueExpr"2\n\x02Op\x12\r\n\tUNDEFINED\x10\x00\x12\x07\n\x03ADD\x10\n\x12\x08\n\x04MULT\x10\x0c\x12\n\n\x06CONCAT\x10\x14"0\n\tArrayExpr\x12#\n\x04vals\x18\x01 \x03(\x0b2\x15.edgir.expr.ValueExpr"[\n\tRangeExpr\x12&\n\x07minimum\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12&\n\x07maximum\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"\x80\x01\n\nStructExpr\x12.\n\x04vals\x18\x01 \x03(\x0b2 .edgir.expr.StructExpr.ValsEntry\x1aB\n\tValsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr:\x028\x01"\xa3\x01\n\x0eIfThenElseExpr\x12#\n\x04cond\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x03tru\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x03fal\x18\x03 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"]\n\x0bExtractExpr\x12(\n\tcontainer\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12$\n\x05index\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"^\n\x0eMapExtractExpr\x12(\n\tcontainer\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x04path\x18\x02 \x01(\x0b2\x14.edgir.ref.LocalPath"\x91\x01\n\rConnectedExpr\x12)\n\nblock_port\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12(\n\tlink_port\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12+\n\x08expanded\x18\x03 \x03(\x0b2\x19.edgir.expr.ConnectedExpr"\x9c\x01\n\x0cExportedExpr\x12,\n\rexterior_port\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x122\n\x13internal_block_port\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12*\n\x08expanded\x18\x03 \x03(\x0b2\x18.edgir.expr.ExportedExpr"S\n\nAssignExpr\x12!\n\x03dst\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12"\n\x03src\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"\x97\x07\n\tValueExpr\x12&\n\x07literal\x18\x01 \x01(\x0b2\x13.edgir.lit.ValueLitH\x00\x12(\n\x06binary\x18\x02 \x01(\x0b2\x16.edgir.expr.BinaryExprH\x00\x12/\n\nbinary_set\x18\x12 \x01(\x0b2\x19.edgir.expr.BinarySetExprH\x00\x12&\n\x05unary\x18\x03 \x01(\x0b2\x15.edgir.expr.UnaryExprH\x00\x12-\n\tunary_set\x18\x04 \x01(\x0b2\x18.edgir.expr.UnarySetExprH\x00\x12&\n\x05array\x18\x06 \x01(\x0b2\x15.edgir.expr.ArrayExprH\x00\x12(\n\x06struct\x18\x07 \x01(\x0b2\x16.edgir.expr.StructExprH\x00\x12&\n\x05range\x18\x08 \x01(\x0b2\x15.edgir.expr.RangeExprH\x00\x120\n\nifThenElse\x18\n \x01(\x0b2\x1a.edgir.expr.IfThenElseExprH\x00\x12*\n\x07extract\x18\x0c \x01(\x0b2\x17.edgir.expr.ExtractExprH\x00\x121\n\x0bmap_extract\x18\x0e \x01(\x0b2\x1a.edgir.expr.MapExtractExprH\x00\x12.\n\tconnected\x18\x0f \x01(\x0b2\x19.edgir.expr.ConnectedExprH\x00\x12,\n\x08exported\x18\x10 \x01(\x0b2\x18.edgir.expr.ExportedExprH\x00\x123\n\x0econnectedArray\x18\x13 \x01(\x0b2\x19.edgir.expr.ConnectedExprH\x00\x121\n\rexportedArray\x18\x14 \x01(\x0b2\x18.edgir.expr.ExportedExprH\x00\x12(\n\x06assign\x18\x11 \x01(\x0b2\x16.edgir.expr.AssignExprH\x00\x122\n\x0eexportedTunnel\x18\x15 \x01(\x0b2\x18.edgir.expr.ExportedExprH\x00\x12.\n\x0cassignTunnel\x18\x16 \x01(\x0b2\x16.edgir.expr.AssignExprH\x00\x12#\n\x03ref\x18c \x01(\x0b2\x14.edgir.ref.LocalPathH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.MetadataB\x06\n\x04exprb\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x10edgir/expr.proto\x12\nedgir.expr\x1a\x0fedgir/ref.proto\x1a\x12edgir/common.proto\x1a\x0fedgir/lit.proto"\xb4\x01\n\tUnaryExpr\x12$\n\x02op\x18\x01 \x01(\x0e2\x18.edgir.expr.UnaryExpr.Op\x12"\n\x03val\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"]\n\x02Op\x12\r\n\tUNDEFINED\x10\x00\x12\n\n\x06NEGATE\x10\x01\x12\x07\n\x03NOT\x10\x02\x12\n\n\x06INVERT\x10\x03\x12\x07\n\x03MIN\x10\x04\x12\x07\n\x03MAX\x10\x05\x12\n\n\x06CENTER\x10\x06\x12\t\n\x05WIDTH\x10\x07"\x9f\x02\n\x0cUnarySetExpr\x12\'\n\x02op\x18\x01 \x01(\x0e2\x1b.edgir.expr.UnarySetExpr.Op\x12#\n\x04vals\x18\x04 \x01(\x0b2\x15.edgir.expr.ValueExpr"\xc0\x01\n\x02Op\x12\r\n\tUNDEFINED\x10\x00\x12\x07\n\x03SUM\x10\x01\x12\x0c\n\x08ALL_TRUE\x10\x02\x12\x0c\n\x08ANY_TRUE\x10\x03\x12\n\n\x06ALL_EQ\x10\x04\x12\x0e\n\nALL_UNIQUE\x10\x05\x12\x0b\n\x07MAXIMUM\x10\n\x12\x0b\n\x07MINIMUM\x10\x0b\x12\x0f\n\x0bSET_EXTRACT\x10\x0c\x12\x10\n\x0cINTERSECTION\x10\r\x12\x08\n\x04HULL\x10\x0e\x12\n\n\x06NEGATE\x10\x14\x12\n\n\x06INVERT\x10\x15\x12\x0b\n\x07FLATTEN\x10\x1e"\xd4\x02\n\nBinaryExpr\x12%\n\x02op\x18\x01 \x01(\x0e2\x19.edgir.expr.BinaryExpr.Op\x12"\n\x03lhs\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x03rhs\x18\x03 \x01(\x0b2\x15.edgir.expr.ValueExpr"\xd6\x01\n\x02Op\x12\r\n\tUNDEFINED\x10\x00\x12\x07\n\x03ADD\x10\n\x12\x08\n\x04MULT\x10\x0c\x12\x0f\n\x0bSHRINK_MULT\x107\x12\x07\n\x03AND\x10\x14\x12\x06\n\x02OR\x10\x15\x12\x07\n\x03XOR\x10\x16\x12\x0b\n\x07IMPLIES\x10\x17\x12\x06\n\x02EQ\x10\x1e\x12\x07\n\x03NEQ\x10\x1f\x12\x06\n\x02GT\x10(\x12\x07\n\x03GTE\x10)\x12\x06\n\x02LT\x10*\x12\x07\n\x03LTE\x10,\x12\x07\n\x03MAX\x10-\x12\x07\n\x03MIN\x10.\x12\x10\n\x0cINTERSECTION\x103\x12\x08\n\x04HULL\x106\x12\n\n\x06WITHIN\x105\x12\t\n\x05RANGE\x10\x01"\xb7\x01\n\rBinarySetExpr\x12(\n\x02op\x18\x01 \x01(\x0e2\x1c.edgir.expr.BinarySetExpr.Op\x12$\n\x05lhset\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x03rhs\x18\x03 \x01(\x0b2\x15.edgir.expr.ValueExpr"2\n\x02Op\x12\r\n\tUNDEFINED\x10\x00\x12\x07\n\x03ADD\x10\n\x12\x08\n\x04MULT\x10\x0c\x12\n\n\x06CONCAT\x10\x14"0\n\tArrayExpr\x12#\n\x04vals\x18\x01 \x03(\x0b2\x15.edgir.expr.ValueExpr"[\n\tRangeExpr\x12&\n\x07minimum\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12&\n\x07maximum\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"\x80\x01\n\nStructExpr\x12.\n\x04vals\x18\x01 \x03(\x0b2 .edgir.expr.StructExpr.ValsEntry\x1aB\n\tValsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr:\x028\x01"\xa3\x01\n\x0eIfThenElseExpr\x12#\n\x04cond\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x03tru\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x03fal\x18\x03 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"]\n\x0bExtractExpr\x12(\n\tcontainer\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12$\n\x05index\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"^\n\x0eMapExtractExpr\x12(\n\tcontainer\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12"\n\x04path\x18\x02 \x01(\x0b2\x14.edgir.ref.LocalPath"\x91\x01\n\rConnectedExpr\x12)\n\nblock_port\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12(\n\tlink_port\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12+\n\x08expanded\x18\x03 \x03(\x0b2\x19.edgir.expr.ConnectedExpr"\x9c\x01\n\x0cExportedExpr\x12,\n\rexterior_port\x18\x01 \x01(\x0b2\x15.edgir.expr.ValueExpr\x122\n\x13internal_block_port\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr\x12*\n\x08expanded\x18\x03 \x03(\x0b2\x18.edgir.expr.ExportedExpr"S\n\nAssignExpr\x12!\n\x03dst\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12"\n\x03src\x18\x02 \x01(\x0b2\x15.edgir.expr.ValueExpr"\x97\x07\n\tValueExpr\x12&\n\x07literal\x18\x01 \x01(\x0b2\x13.edgir.lit.ValueLitH\x00\x12(\n\x06binary\x18\x02 \x01(\x0b2\x16.edgir.expr.BinaryExprH\x00\x12/\n\nbinary_set\x18\x12 \x01(\x0b2\x19.edgir.expr.BinarySetExprH\x00\x12&\n\x05unary\x18\x03 \x01(\x0b2\x15.edgir.expr.UnaryExprH\x00\x12-\n\tunary_set\x18\x04 \x01(\x0b2\x18.edgir.expr.UnarySetExprH\x00\x12&\n\x05array\x18\x06 \x01(\x0b2\x15.edgir.expr.ArrayExprH\x00\x12(\n\x06struct\x18\x07 \x01(\x0b2\x16.edgir.expr.StructExprH\x00\x12&\n\x05range\x18\x08 \x01(\x0b2\x15.edgir.expr.RangeExprH\x00\x120\n\nifThenElse\x18\n \x01(\x0b2\x1a.edgir.expr.IfThenElseExprH\x00\x12*\n\x07extract\x18\x0c \x01(\x0b2\x17.edgir.expr.ExtractExprH\x00\x121\n\x0bmap_extract\x18\x0e \x01(\x0b2\x1a.edgir.expr.MapExtractExprH\x00\x12.\n\tconnected\x18\x0f \x01(\x0b2\x19.edgir.expr.ConnectedExprH\x00\x12,\n\x08exported\x18\x10 \x01(\x0b2\x18.edgir.expr.ExportedExprH\x00\x123\n\x0econnectedArray\x18\x13 \x01(\x0b2\x19.edgir.expr.ConnectedExprH\x00\x121\n\rexportedArray\x18\x14 \x01(\x0b2\x18.edgir.expr.ExportedExprH\x00\x12(\n\x06assign\x18\x11 \x01(\x0b2\x16.edgir.expr.AssignExprH\x00\x122\n\x0eexportedTunnel\x18\x15 \x01(\x0b2\x18.edgir.expr.ExportedExprH\x00\x12.\n\x0cassignTunnel\x18\x16 \x01(\x0b2\x16.edgir.expr.AssignExprH\x00\x12#\n\x03ref\x18c \x01(\x0b2\x14.edgir.ref.LocalPathH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.MetadataB\x06\n\x04exprb\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.expr_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.expr_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _STRUCTEXPR_VALSENTRY._options = None - _STRUCTEXPR_VALSENTRY._serialized_options = b'8\x01' + _STRUCTEXPR_VALSENTRY._serialized_options = b"8\x01" _UNARYEXPR._serialized_start = 87 _UNARYEXPR._serialized_end = 267 _UNARYEXPR_OP._serialized_start = 174 @@ -51,4 +56,4 @@ _ASSIGNEXPR._serialized_start = 2026 _ASSIGNEXPR._serialized_end = 2109 _VALUEEXPR._serialized_start = 2112 - _VALUEEXPR._serialized_end = 3031 \ No newline at end of file + _VALUEEXPR._serialized_end = 3031 diff --git a/edg/edgir/expr_pb2.pyi b/edg/edgir/expr_pb2.pyi index 8b243daf6..2c6d1cad8 100644 --- a/edg/edgir/expr_pb2.pyi +++ b/edg/edgir/expr_pb2.pyi @@ -25,6 +25,7 @@ in a small number number of places to help classify types into groups. I don't think we should be using sorts at all in this module, but it's defined for the sake on completeness. """ + import builtins import collections.abc from .. import edgir @@ -34,6 +35,7 @@ import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import sys import typing + if sys.version_info >= (3, 10): import typing as typing_extensions else: @@ -45,60 +47,55 @@ class UnaryExpr(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor class _Op: - ValueType = typing.NewType('ValueType', builtins.int) + ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType - class _OpEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[UnaryExpr._Op.ValueType], builtins.type): + class _OpEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[UnaryExpr._Op.ValueType], builtins.type + ): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor UNDEFINED: UnaryExpr._Op.ValueType NEGATE: UnaryExpr._Op.ValueType - '* Negate :: Numeric a => a -> a\n :: Numeric a => Range a -> Range a\n ' + "* Negate :: Numeric a => a -> a\n :: Numeric a => Range a -> Range a\n " NOT: UnaryExpr._Op.ValueType - '* Not :: Bool -> Bool' + "* Not :: Bool -> Bool" INVERT: UnaryExpr._Op.ValueType - '* Invert :: Float -> Float\n :: Range Float -> Range Float\n ' + "* Invert :: Float -> Float\n :: Range Float -> Range Float\n " MIN: UnaryExpr._Op.ValueType - '* Min :: Range a -> a' + "* Min :: Range a -> a" MAX: UnaryExpr._Op.ValueType - '* Max :: Range a -> a' + "* Max :: Range a -> a" CENTER: UnaryExpr._Op.ValueType - '* Center :: Range a -> a' + "* Center :: Range a -> a" WIDTH: UnaryExpr._Op.ValueType - '* Width :: Range a -> a' + "* Width :: Range a -> a" - class Op(_Op, metaclass=_OpEnumTypeWrapper): - ... + class Op(_Op, metaclass=_OpEnumTypeWrapper): ... UNDEFINED: UnaryExpr.Op.ValueType NEGATE: UnaryExpr.Op.ValueType - '* Negate :: Numeric a => a -> a\n :: Numeric a => Range a -> Range a\n ' + "* Negate :: Numeric a => a -> a\n :: Numeric a => Range a -> Range a\n " NOT: UnaryExpr.Op.ValueType - '* Not :: Bool -> Bool' + "* Not :: Bool -> Bool" INVERT: UnaryExpr.Op.ValueType - '* Invert :: Float -> Float\n :: Range Float -> Range Float\n ' + "* Invert :: Float -> Float\n :: Range Float -> Range Float\n " MIN: UnaryExpr.Op.ValueType - '* Min :: Range a -> a' + "* Min :: Range a -> a" MAX: UnaryExpr.Op.ValueType - '* Max :: Range a -> a' + "* Max :: Range a -> a" CENTER: UnaryExpr.Op.ValueType - '* Center :: Range a -> a' + "* Center :: Range a -> a" WIDTH: UnaryExpr.Op.ValueType - '* Width :: Range a -> a' + "* Width :: Range a -> a" OP_FIELD_NUMBER: builtins.int VAL_FIELD_NUMBER: builtins.int op: global___UnaryExpr.Op.ValueType @property - def val(self) -> global___ValueExpr: - ... - - def __init__(self, *, op: global___UnaryExpr.Op.ValueType=..., val: global___ValueExpr | None=...) -> None: - ... + def val(self) -> global___ValueExpr: ... + def __init__(self, *, op: global___UnaryExpr.Op.ValueType = ..., val: global___ValueExpr | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["val", b"val"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["op", b"op", "val", b"val"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['val', b'val']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['op', b'op', 'val', b'val']) -> None: - ... global___UnaryExpr = UnaryExpr @typing_extensions.final @@ -106,84 +103,81 @@ class UnarySetExpr(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor class _Op: - ValueType = typing.NewType('ValueType', builtins.int) + ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType - class _OpEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[UnarySetExpr._Op.ValueType], builtins.type): + class _OpEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[UnarySetExpr._Op.ValueType], builtins.type + ): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor UNDEFINED: UnarySetExpr._Op.ValueType SUM: UnarySetExpr._Op.ValueType - '* Sum :: (Numeric a) => Set a -> a\n :: (Numeric a) => Set (Range a) -> Range a\n\n Sum({}) = 0\n ' + "* Sum :: (Numeric a) => Set a -> a\n :: (Numeric a) => Set (Range a) -> Range a\n\n Sum({}) = 0\n " ALL_TRUE: UnarySetExpr._Op.ValueType - '* All :: Set Bool -> Bool\n\n All inputs are true\n All({}) = True\n ' + "* All :: Set Bool -> Bool\n\n All inputs are true\n All({}) = True\n " ANY_TRUE: UnarySetExpr._Op.ValueType - '* Any :: Set Bool -> Bool\n\n Any of the inputs are true\n Any({}) = False\n ' + "* Any :: Set Bool -> Bool\n\n Any of the inputs are true\n Any({}) = False\n " ALL_EQ: UnarySetExpr._Op.ValueType - '* AllEq :: (Equality a) => Set a -> Bool\n\n AllEq({}) = True\n ' + "* AllEq :: (Equality a) => Set a -> Bool\n\n AllEq({}) = True\n " ALL_UNIQUE: UnarySetExpr._Op.ValueType - '* AllUnique :: (Equality a) => Set a -> Bool\n\n AllUnique(EmptySet) = True\n ' + "* AllUnique :: (Equality a) => Set a -> Bool\n\n AllUnique(EmptySet) = True\n " MAXIMUM: UnarySetExpr._Op.ValueType - 'SIZE = 6;\n\n * Maximum :: (Ordered a) => Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid.\n ' + "SIZE = 6;\n\n * Maximum :: (Ordered a) => Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid.\n " MINIMUM: UnarySetExpr._Op.ValueType - '* Minimum :: (Ordered a) => Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid.\n ' + "* Minimum :: (Ordered a) => Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid.\n " SET_EXTRACT: UnarySetExpr._Op.ValueType - '* SetExtract :: Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid. In addition this assumes all values in the set are equal.\n ' + "* SetExtract :: Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid. In addition this assumes all values in the set are equal.\n " INTERSECTION: UnarySetExpr._Op.ValueType - '* Intersection :: Set (Range a) -> Range a\n\n May produce an empty range.\n Intersection({}) = [-inf, +inf]\n ' + "* Intersection :: Set (Range a) -> Range a\n\n May produce an empty range.\n Intersection({}) = [-inf, +inf]\n " HULL: UnarySetExpr._Op.ValueType - '* Hull :: Set (Range a) -> Range a\n Returns the convex hull (union with all the inner missing bits filled in)\n Hull({}) = EmptyRange\n ' + "* Hull :: Set (Range a) -> Range a\n Returns the convex hull (union with all the inner missing bits filled in)\n Hull({}) = EmptyRange\n " NEGATE: UnarySetExpr._Op.ValueType - '* Negate :: Numeric a => Set a -> Set a\n :: Numeric a => Set (Range a) -> Set (Range a)\n\n Pointwise negate\n ' + "* Negate :: Numeric a => Set a -> Set a\n :: Numeric a => Set (Range a) -> Set (Range a)\n\n Pointwise negate\n " INVERT: UnarySetExpr._Op.ValueType - '* Invert :: Set Float -> Set Float\n :: Set (Range Float) -> Set (Range Float)\n\n Pointwise Invert\n ' + "* Invert :: Set Float -> Set Float\n :: Set (Range Float) -> Set (Range Float)\n\n Pointwise Invert\n " FLATTEN: UnarySetExpr._Op.ValueType - 'Flatten[A] : Set[Set[A]] -> Set[A]\n Given an array of array of elements, flattens the inner array.\n Alternatively stated, concatenates all of the elements of the outer arrary\n ' + "Flatten[A] : Set[Set[A]] -> Set[A]\n Given an array of array of elements, flattens the inner array.\n Alternatively stated, concatenates all of the elements of the outer arrary\n " - class Op(_Op, metaclass=_OpEnumTypeWrapper): - ... + class Op(_Op, metaclass=_OpEnumTypeWrapper): ... UNDEFINED: UnarySetExpr.Op.ValueType SUM: UnarySetExpr.Op.ValueType - '* Sum :: (Numeric a) => Set a -> a\n :: (Numeric a) => Set (Range a) -> Range a\n\n Sum({}) = 0\n ' + "* Sum :: (Numeric a) => Set a -> a\n :: (Numeric a) => Set (Range a) -> Range a\n\n Sum({}) = 0\n " ALL_TRUE: UnarySetExpr.Op.ValueType - '* All :: Set Bool -> Bool\n\n All inputs are true\n All({}) = True\n ' + "* All :: Set Bool -> Bool\n\n All inputs are true\n All({}) = True\n " ANY_TRUE: UnarySetExpr.Op.ValueType - '* Any :: Set Bool -> Bool\n\n Any of the inputs are true\n Any({}) = False\n ' + "* Any :: Set Bool -> Bool\n\n Any of the inputs are true\n Any({}) = False\n " ALL_EQ: UnarySetExpr.Op.ValueType - '* AllEq :: (Equality a) => Set a -> Bool\n\n AllEq({}) = True\n ' + "* AllEq :: (Equality a) => Set a -> Bool\n\n AllEq({}) = True\n " ALL_UNIQUE: UnarySetExpr.Op.ValueType - '* AllUnique :: (Equality a) => Set a -> Bool\n\n AllUnique(EmptySet) = True\n ' + "* AllUnique :: (Equality a) => Set a -> Bool\n\n AllUnique(EmptySet) = True\n " MAXIMUM: UnarySetExpr.Op.ValueType - 'SIZE = 6;\n\n * Maximum :: (Ordered a) => Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid.\n ' + "SIZE = 6;\n\n * Maximum :: (Ordered a) => Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid.\n " MINIMUM: UnarySetExpr.Op.ValueType - '* Minimum :: (Ordered a) => Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid.\n ' + "* Minimum :: (Ordered a) => Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid.\n " SET_EXTRACT: UnarySetExpr.Op.ValueType - '* SetExtract :: Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid. In addition this assumes all values in the set are equal.\n ' + "* SetExtract :: Set a -> a\n\n This op requires that the non-emptyness of the relevant set is assured\n before being valid. In addition this assumes all values in the set are equal.\n " INTERSECTION: UnarySetExpr.Op.ValueType - '* Intersection :: Set (Range a) -> Range a\n\n May produce an empty range.\n Intersection({}) = [-inf, +inf]\n ' + "* Intersection :: Set (Range a) -> Range a\n\n May produce an empty range.\n Intersection({}) = [-inf, +inf]\n " HULL: UnarySetExpr.Op.ValueType - '* Hull :: Set (Range a) -> Range a\n Returns the convex hull (union with all the inner missing bits filled in)\n Hull({}) = EmptyRange\n ' + "* Hull :: Set (Range a) -> Range a\n Returns the convex hull (union with all the inner missing bits filled in)\n Hull({}) = EmptyRange\n " NEGATE: UnarySetExpr.Op.ValueType - '* Negate :: Numeric a => Set a -> Set a\n :: Numeric a => Set (Range a) -> Set (Range a)\n\n Pointwise negate\n ' + "* Negate :: Numeric a => Set a -> Set a\n :: Numeric a => Set (Range a) -> Set (Range a)\n\n Pointwise negate\n " INVERT: UnarySetExpr.Op.ValueType - '* Invert :: Set Float -> Set Float\n :: Set (Range Float) -> Set (Range Float)\n\n Pointwise Invert\n ' + "* Invert :: Set Float -> Set Float\n :: Set (Range Float) -> Set (Range Float)\n\n Pointwise Invert\n " FLATTEN: UnarySetExpr.Op.ValueType - 'Flatten[A] : Set[Set[A]] -> Set[A]\n Given an array of array of elements, flattens the inner array.\n Alternatively stated, concatenates all of the elements of the outer arrary\n ' + "Flatten[A] : Set[Set[A]] -> Set[A]\n Given an array of array of elements, flattens the inner array.\n Alternatively stated, concatenates all of the elements of the outer arrary\n " OP_FIELD_NUMBER: builtins.int VALS_FIELD_NUMBER: builtins.int op: global___UnarySetExpr.Op.ValueType @property - def vals(self) -> global___ValueExpr: - ... - - def __init__(self, *, op: global___UnarySetExpr.Op.ValueType=..., vals: global___ValueExpr | None=...) -> None: - ... + def vals(self) -> global___ValueExpr: ... + def __init__( + self, *, op: global___UnarySetExpr.Op.ValueType = ..., vals: global___ValueExpr | None = ... + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["vals", b"vals"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["op", b"op", "vals", b"vals"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['vals', b'vals']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['op', b'op', 'vals', b'vals']) -> None: - ... global___UnarySetExpr = UnarySetExpr @typing_extensions.final @@ -191,113 +185,112 @@ class BinaryExpr(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor class _Op: - ValueType = typing.NewType('ValueType', builtins.int) + ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType - class _OpEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BinaryExpr._Op.ValueType], builtins.type): + class _OpEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BinaryExpr._Op.ValueType], builtins.type + ): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor UNDEFINED: BinaryExpr._Op.ValueType ADD: BinaryExpr._Op.ValueType - '* Add :: Numeric a => (lhs :: a, rhs :: a) -> a\n :: Numeric a => (lhs :: a, rhs :: Range a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: Range a) -> Range a\n ' + "* Add :: Numeric a => (lhs :: a, rhs :: a) -> a\n :: Numeric a => (lhs :: a, rhs :: Range a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: Range a) -> Range a\n " MULT: BinaryExpr._Op.ValueType - 'SUB = 11; // Use ADD and NEGATE instead\n\n * Mult :: Numeric a => (lhs :: a, rhs :: a) -> a\n :: Numeric a => (lhs :: a, rhs :: Range a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: Range a) -> Range a\n ' + "SUB = 11; // Use ADD and NEGATE instead\n\n * Mult :: Numeric a => (lhs :: a, rhs :: a) -> a\n :: Numeric a => (lhs :: a, rhs :: Range a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: Range a) -> Range a\n " SHRINK_MULT: BinaryExpr._Op.ValueType - 'A shrinking multiply operation for two Range types. Not commutative.\n See the documentation for shrink_multiply in the Python core HDL code for details.\n ' + "A shrinking multiply operation for two Range types. Not commutative.\n See the documentation for shrink_multiply in the Python core HDL code for details.\n " AND: BinaryExpr._Op.ValueType - 'DIV = 13; // Use MULT and INVERT instead\n\n * And :: (lhs :: Bool, rhs :: Bool) -> Bool\n ' + "DIV = 13; // Use MULT and INVERT instead\n\n * And :: (lhs :: Bool, rhs :: Bool) -> Bool\n " OR: BinaryExpr._Op.ValueType - '* Or :: (lhs :: Bool, rhs :: Bool) -> Bool' + "* Or :: (lhs :: Bool, rhs :: Bool) -> Bool" XOR: BinaryExpr._Op.ValueType - '* Xor :: (lhs :: Bool, rhs :: Bool) -> Bool' + "* Xor :: (lhs :: Bool, rhs :: Bool) -> Bool" IMPLIES: BinaryExpr._Op.ValueType - '* Implies :: (lhs :: Bool, rhs :: Bool) -> Bool' + "* Implies :: (lhs :: Bool, rhs :: Bool) -> Bool" EQ: BinaryExpr._Op.ValueType - 'IFF = 24; // Use EQ instead\n\n * Eq :: (Equality a) => (lhs :: a, rhs :: a) -> Bool\n ' + "IFF = 24; // Use EQ instead\n\n * Eq :: (Equality a) => (lhs :: a, rhs :: a) -> Bool\n " NEQ: BinaryExpr._Op.ValueType - '* Neq :: (Equality a) => (lhs :: a, rhs : a) -> Bool' + "* Neq :: (Equality a) => (lhs :: a, rhs : a) -> Bool" GT: BinaryExpr._Op.ValueType - '* GT :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool' + "* GT :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool" GTE: BinaryExpr._Op.ValueType - '* GTE :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool' + "* GTE :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool" LT: BinaryExpr._Op.ValueType - '* LT :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool' + "* LT :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool" LTE: BinaryExpr._Op.ValueType - '* LTE :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool' + "* LTE :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool" MAX: BinaryExpr._Op.ValueType - '* Max :: (Comparable a) => (lhs :: a, rhs :: a) -> a' + "* Max :: (Comparable a) => (lhs :: a, rhs :: a) -> a" MIN: BinaryExpr._Op.ValueType - '* Min :: (Comparable a) => (lhs :: a, rhs :: a) -> a' + "* Min :: (Comparable a) => (lhs :: a, rhs :: a) -> a" INTERSECTION: BinaryExpr._Op.ValueType - 'UNION = 50;\n\n * Intersection :: (Numeric a) => (lhs : Range a, rhs : Range a) -> Range a\n ' + "UNION = 50;\n\n * Intersection :: (Numeric a) => (lhs : Range a, rhs : Range a) -> Range a\n " HULL: BinaryExpr._Op.ValueType - '* Hull :: (lhs :: Range a, rhs :: Range a) -> Range a\n Given two input ranges, returns the convex hull (union with\n all the inner missing bits filled in)\n ' + "* Hull :: (lhs :: Range a, rhs :: Range a) -> Range a\n Given two input ranges, returns the convex hull (union with\n all the inner missing bits filled in)\n " WITHIN: BinaryExpr._Op.ValueType "INTERSECTS = 52;\n\n * Within :: (Numeric a) => (lhs :: Range a, rhs :: Range a) -> Bool\n :: (Numeric a) => (lhs :: a, rhs :: Range a) -> Bool\n\n Whether the lhs range or point is entirely within (contained by) the rhs.\n Used to be named SUBSET changed to a name that doesn't also imply a set op.\n " RANGE: BinaryExpr._Op.ValueType - '* Range :: (Comparable a) => (lower :: a, upper :: a) -> Range a' + "* Range :: (Comparable a) => (lower :: a, upper :: a) -> Range a" - class Op(_Op, metaclass=_OpEnumTypeWrapper): - ... + class Op(_Op, metaclass=_OpEnumTypeWrapper): ... UNDEFINED: BinaryExpr.Op.ValueType ADD: BinaryExpr.Op.ValueType - '* Add :: Numeric a => (lhs :: a, rhs :: a) -> a\n :: Numeric a => (lhs :: a, rhs :: Range a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: Range a) -> Range a\n ' + "* Add :: Numeric a => (lhs :: a, rhs :: a) -> a\n :: Numeric a => (lhs :: a, rhs :: Range a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: Range a) -> Range a\n " MULT: BinaryExpr.Op.ValueType - 'SUB = 11; // Use ADD and NEGATE instead\n\n * Mult :: Numeric a => (lhs :: a, rhs :: a) -> a\n :: Numeric a => (lhs :: a, rhs :: Range a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: Range a) -> Range a\n ' + "SUB = 11; // Use ADD and NEGATE instead\n\n * Mult :: Numeric a => (lhs :: a, rhs :: a) -> a\n :: Numeric a => (lhs :: a, rhs :: Range a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: a) -> Range a\n :: Numeric a => (lhs :: Range a, rhs :: Range a) -> Range a\n " SHRINK_MULT: BinaryExpr.Op.ValueType - 'A shrinking multiply operation for two Range types. Not commutative.\n See the documentation for shrink_multiply in the Python core HDL code for details.\n ' + "A shrinking multiply operation for two Range types. Not commutative.\n See the documentation for shrink_multiply in the Python core HDL code for details.\n " AND: BinaryExpr.Op.ValueType - 'DIV = 13; // Use MULT and INVERT instead\n\n * And :: (lhs :: Bool, rhs :: Bool) -> Bool\n ' + "DIV = 13; // Use MULT and INVERT instead\n\n * And :: (lhs :: Bool, rhs :: Bool) -> Bool\n " OR: BinaryExpr.Op.ValueType - '* Or :: (lhs :: Bool, rhs :: Bool) -> Bool' + "* Or :: (lhs :: Bool, rhs :: Bool) -> Bool" XOR: BinaryExpr.Op.ValueType - '* Xor :: (lhs :: Bool, rhs :: Bool) -> Bool' + "* Xor :: (lhs :: Bool, rhs :: Bool) -> Bool" IMPLIES: BinaryExpr.Op.ValueType - '* Implies :: (lhs :: Bool, rhs :: Bool) -> Bool' + "* Implies :: (lhs :: Bool, rhs :: Bool) -> Bool" EQ: BinaryExpr.Op.ValueType - 'IFF = 24; // Use EQ instead\n\n * Eq :: (Equality a) => (lhs :: a, rhs :: a) -> Bool\n ' + "IFF = 24; // Use EQ instead\n\n * Eq :: (Equality a) => (lhs :: a, rhs :: a) -> Bool\n " NEQ: BinaryExpr.Op.ValueType - '* Neq :: (Equality a) => (lhs :: a, rhs : a) -> Bool' + "* Neq :: (Equality a) => (lhs :: a, rhs : a) -> Bool" GT: BinaryExpr.Op.ValueType - '* GT :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool' + "* GT :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool" GTE: BinaryExpr.Op.ValueType - '* GTE :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool' + "* GTE :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool" LT: BinaryExpr.Op.ValueType - '* LT :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool' + "* LT :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool" LTE: BinaryExpr.Op.ValueType - '* LTE :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool' + "* LTE :: (Comparable a) => (lhs :: a, rhs :: a) -> Bool" MAX: BinaryExpr.Op.ValueType - '* Max :: (Comparable a) => (lhs :: a, rhs :: a) -> a' + "* Max :: (Comparable a) => (lhs :: a, rhs :: a) -> a" MIN: BinaryExpr.Op.ValueType - '* Min :: (Comparable a) => (lhs :: a, rhs :: a) -> a' + "* Min :: (Comparable a) => (lhs :: a, rhs :: a) -> a" INTERSECTION: BinaryExpr.Op.ValueType - 'UNION = 50;\n\n * Intersection :: (Numeric a) => (lhs : Range a, rhs : Range a) -> Range a\n ' + "UNION = 50;\n\n * Intersection :: (Numeric a) => (lhs : Range a, rhs : Range a) -> Range a\n " HULL: BinaryExpr.Op.ValueType - '* Hull :: (lhs :: Range a, rhs :: Range a) -> Range a\n Given two input ranges, returns the convex hull (union with\n all the inner missing bits filled in)\n ' + "* Hull :: (lhs :: Range a, rhs :: Range a) -> Range a\n Given two input ranges, returns the convex hull (union with\n all the inner missing bits filled in)\n " WITHIN: BinaryExpr.Op.ValueType "INTERSECTS = 52;\n\n * Within :: (Numeric a) => (lhs :: Range a, rhs :: Range a) -> Bool\n :: (Numeric a) => (lhs :: a, rhs :: Range a) -> Bool\n\n Whether the lhs range or point is entirely within (contained by) the rhs.\n Used to be named SUBSET changed to a name that doesn't also imply a set op.\n " RANGE: BinaryExpr.Op.ValueType - '* Range :: (Comparable a) => (lower :: a, upper :: a) -> Range a' + "* Range :: (Comparable a) => (lower :: a, upper :: a) -> Range a" OP_FIELD_NUMBER: builtins.int LHS_FIELD_NUMBER: builtins.int RHS_FIELD_NUMBER: builtins.int op: global___BinaryExpr.Op.ValueType @property - def lhs(self) -> global___ValueExpr: - ... - + def lhs(self) -> global___ValueExpr: ... @property - def rhs(self) -> global___ValueExpr: - ... - - def __init__(self, *, op: global___BinaryExpr.Op.ValueType=..., lhs: global___ValueExpr | None=..., rhs: global___ValueExpr | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['lhs', b'lhs', 'rhs', b'rhs']) -> builtins.bool: - ... + def rhs(self) -> global___ValueExpr: ... + def __init__( + self, + *, + op: global___BinaryExpr.Op.ValueType = ..., + lhs: global___ValueExpr | None = ..., + rhs: global___ValueExpr | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["lhs", b"lhs", "rhs", b"rhs"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["lhs", b"lhs", "op", b"op", "rhs", b"rhs"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['lhs', b'lhs', 'op', b'op', 'rhs', b'rhs']) -> None: - ... global___BinaryExpr = BinaryExpr @typing_extensions.final @@ -305,66 +298,64 @@ class BinarySetExpr(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor class _Op: - ValueType = typing.NewType('ValueType', builtins.int) + ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType - class _OpEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BinarySetExpr._Op.ValueType], builtins.type): + class _OpEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BinarySetExpr._Op.ValueType], builtins.type + ): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor UNDEFINED: BinarySetExpr._Op.ValueType ADD: BinarySetExpr._Op.ValueType - '* Add :: Numeric a => (lhset : Set a, rhs : a) -> Set a\n :: Numeric a => (lhset : Set a, rhs : Range a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : Range a) -> Set (Range a)\n ' + "* Add :: Numeric a => (lhset : Set a, rhs : a) -> Set a\n :: Numeric a => (lhset : Set a, rhs : Range a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : Range a) -> Set (Range a)\n " MULT: BinarySetExpr._Op.ValueType - '* Mult :: Numeric a => (lhset : Set a, rhs : a) -> Set a\n :: Numeric a => (lhset : Set a, rhs : Range a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : Range a) -> Set (Range a)\n ' + "* Mult :: Numeric a => (lhset : Set a, rhs : a) -> Set a\n :: Numeric a => (lhset : Set a, rhs : Range a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : Range a) -> Set (Range a)\n " CONCAT: BinarySetExpr._Op.ValueType - 'String concatenate operator\n Concatenate : (lhs: String, rhss: Set[String]) -> Set[String] (prepend lhs to all elements)\n : (lhss: Set[String], rhs: String) -> Set[String] (append rhs to all elements)\n ' + "String concatenate operator\n Concatenate : (lhs: String, rhss: Set[String]) -> Set[String] (prepend lhs to all elements)\n : (lhss: Set[String], rhs: String) -> Set[String] (append rhs to all elements)\n " - class Op(_Op, metaclass=_OpEnumTypeWrapper): - ... + class Op(_Op, metaclass=_OpEnumTypeWrapper): ... UNDEFINED: BinarySetExpr.Op.ValueType ADD: BinarySetExpr.Op.ValueType - '* Add :: Numeric a => (lhset : Set a, rhs : a) -> Set a\n :: Numeric a => (lhset : Set a, rhs : Range a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : Range a) -> Set (Range a)\n ' + "* Add :: Numeric a => (lhset : Set a, rhs : a) -> Set a\n :: Numeric a => (lhset : Set a, rhs : Range a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : Range a) -> Set (Range a)\n " MULT: BinarySetExpr.Op.ValueType - '* Mult :: Numeric a => (lhset : Set a, rhs : a) -> Set a\n :: Numeric a => (lhset : Set a, rhs : Range a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : Range a) -> Set (Range a)\n ' + "* Mult :: Numeric a => (lhset : Set a, rhs : a) -> Set a\n :: Numeric a => (lhset : Set a, rhs : Range a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : a) -> Set (Range a)\n :: Numeric a => (lhset : Set (Range a), rhs : Range a) -> Set (Range a)\n " CONCAT: BinarySetExpr.Op.ValueType - 'String concatenate operator\n Concatenate : (lhs: String, rhss: Set[String]) -> Set[String] (prepend lhs to all elements)\n : (lhss: Set[String], rhs: String) -> Set[String] (append rhs to all elements)\n ' + "String concatenate operator\n Concatenate : (lhs: String, rhss: Set[String]) -> Set[String] (prepend lhs to all elements)\n : (lhss: Set[String], rhs: String) -> Set[String] (append rhs to all elements)\n " OP_FIELD_NUMBER: builtins.int LHSET_FIELD_NUMBER: builtins.int RHS_FIELD_NUMBER: builtins.int op: global___BinarySetExpr.Op.ValueType @property - def lhset(self) -> global___ValueExpr: - ... - + def lhset(self) -> global___ValueExpr: ... @property - def rhs(self) -> global___ValueExpr: - ... - - def __init__(self, *, op: global___BinarySetExpr.Op.ValueType=..., lhset: global___ValueExpr | None=..., rhs: global___ValueExpr | None=...) -> None: - ... + def rhs(self) -> global___ValueExpr: ... + def __init__( + self, + *, + op: global___BinarySetExpr.Op.ValueType = ..., + lhset: global___ValueExpr | None = ..., + rhs: global___ValueExpr | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["lhset", b"lhset", "rhs", b"rhs"]) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["lhset", b"lhset", "op", b"op", "rhs", b"rhs"] + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['lhset', b'lhset', 'rhs', b'rhs']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['lhset', b'lhset', 'op', b'op', 'rhs', b'rhs']) -> None: - ... global___BinarySetExpr = BinarySetExpr @typing_extensions.final class ArrayExpr(google.protobuf.message.Message): """* Creates an array from element exprs""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor VALS_FIELD_NUMBER: builtins.int @property - def vals(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ValueExpr]: - ... + def vals(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ValueExpr]: ... + def __init__(self, *, vals: collections.abc.Iterable[global___ValueExpr] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["vals", b"vals"]) -> None: ... - def __init__(self, *, vals: collections.abc.Iterable[global___ValueExpr] | None=...) -> None: - ... - - def ClearField(self, field_name: typing_extensions.Literal['vals', b'vals']) -> None: - ... global___ArrayExpr = ArrayExpr @typing_extensions.final @@ -372,26 +363,25 @@ class RangeExpr(google.protobuf.message.Message): """* Ranges have an expression form, allowing you to constrain them without specifying them fully """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor MINIMUM_FIELD_NUMBER: builtins.int MAXIMUM_FIELD_NUMBER: builtins.int @property - def minimum(self) -> global___ValueExpr: - ... - + def minimum(self) -> global___ValueExpr: ... @property - def maximum(self) -> global___ValueExpr: - ... - - def __init__(self, *, minimum: global___ValueExpr | None=..., maximum: global___ValueExpr | None=...) -> None: - ... + def maximum(self) -> global___ValueExpr: ... + def __init__( + self, *, minimum: global___ValueExpr | None = ..., maximum: global___ValueExpr | None = ... + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["maximum", b"maximum", "minimum", b"minimum"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["maximum", b"maximum", "minimum", b"minimum"] + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['maximum', b'maximum', 'minimum', b'minimum']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['maximum', b'maximum', 'minimum', b'minimum']) -> None: - ... global___RangeExpr = RangeExpr @typing_extensions.final @@ -399,6 +389,7 @@ class StructExpr(google.protobuf.message.Message): """* Structs have an expression form, allowing you to constrain them without specifying them fully """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final @@ -409,33 +400,24 @@ class StructExpr(google.protobuf.message.Message): key: builtins.str @property - def value(self) -> global___ValueExpr: - ... - - def __init__(self, *, key: builtins.str=..., value: global___ValueExpr | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... + def value(self) -> global___ValueExpr: ... + def __init__(self, *, key: builtins.str = ..., value: global___ValueExpr | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['key', b'key', 'value', b'value']) -> None: - ... VALS_FIELD_NUMBER: builtins.int @property - def vals(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ValueExpr]: - ... + def vals(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ValueExpr]: ... + def __init__(self, *, vals: collections.abc.Mapping[builtins.str, global___ValueExpr] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["vals", b"vals"]) -> None: ... - def __init__(self, *, vals: collections.abc.Mapping[builtins.str, global___ValueExpr] | None=...) -> None: - ... - - def ClearField(self, field_name: typing_extensions.Literal['vals', b'vals']) -> None: - ... global___StructExpr = StructExpr @typing_extensions.final class IfThenElseExpr(google.protobuf.message.Message): """* IfThenElse :: (cond :: Bool, tru :: a, fal :: a) -> a""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor COND_FIELD_NUMBER: builtins.int TRU_FIELD_NUMBER: builtins.int @@ -443,29 +425,28 @@ class IfThenElseExpr(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def cond(self) -> global___ValueExpr: - ... - + def cond(self) -> global___ValueExpr: ... @property - def tru(self) -> global___ValueExpr: - ... - + def tru(self) -> global___ValueExpr: ... @property - def fal(self) -> global___ValueExpr: - ... - + def fal(self) -> global___ValueExpr: ... @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, cond: global___ValueExpr | None=..., tru: global___ValueExpr | None=..., fal: global___ValueExpr | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + cond: global___ValueExpr | None = ..., + tru: global___ValueExpr | None = ..., + fal: global___ValueExpr | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["cond", b"cond", "fal", b"fal", "meta", b"meta", "tru", b"tru"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["cond", b"cond", "fal", b"fal", "meta", b"meta", "tru", b"tru"] + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['cond', b'cond', 'fal', b'fal', 'meta', b'meta', 'tru', b'tru']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['cond', b'cond', 'fal', b'fal', 'meta', b'meta', 'tru', b'tru']) -> None: - ... global___IfThenElseExpr = IfThenElseExpr @typing_extensions.final @@ -474,56 +455,52 @@ class ExtractExpr(google.protobuf.message.Message): Extract :: (container :: Struct{index :: a}, index :: string) -> a Extract :: (container :: Range a , index :: {"minimum"|"maximum"}) -> a """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor CONTAINER_FIELD_NUMBER: builtins.int INDEX_FIELD_NUMBER: builtins.int @property - def container(self) -> global___ValueExpr: - ... - + def container(self) -> global___ValueExpr: ... @property - def index(self) -> global___ValueExpr: - ... - - def __init__(self, *, container: global___ValueExpr | None=..., index: global___ValueExpr | None=...) -> None: - ... + def index(self) -> global___ValueExpr: ... + def __init__( + self, *, container: global___ValueExpr | None = ..., index: global___ValueExpr | None = ... + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["container", b"container", "index", b"index"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["container", b"container", "index", b"index"] + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['container', b'container', 'index', b'index']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['container', b'container', 'index', b'index']) -> None: - ... global___ExtractExpr = ExtractExpr @typing_extensions.final class MapExtractExpr(google.protobuf.message.Message): """/** MapExtract :: (container :: Array a , path :: LocalRef{from :: a, to :: b}) -> Array b - MapExtract :: (container :: Set a , path :: LocalRef{from :: a, to :: b}) -> Set b + MapExtract :: (container :: Set a , path :: LocalRef{from :: a, to :: b}) -> Set b - This expression can map over a container and return a container of - the relevant subexpression determined by a path. */ + This expression can map over a container and return a container of + the relevant subexpression determined by a path. */ """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor CONTAINER_FIELD_NUMBER: builtins.int PATH_FIELD_NUMBER: builtins.int @property - def container(self) -> global___ValueExpr: - ... - + def container(self) -> global___ValueExpr: ... @property - def path(self) -> edgir.ref_pb2.LocalPath: - ... - - def __init__(self, *, container: global___ValueExpr | None=..., path: edgir.ref_pb2.LocalPath | None=...) -> None: - ... + def path(self) -> edgir.ref_pb2.LocalPath: ... + def __init__( + self, *, container: global___ValueExpr | None = ..., path: edgir.ref_pb2.LocalPath | None = ... + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["container", b"container", "path", b"path"] + ) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["container", b"container", "path", b"path"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['container', b'container', 'path', b'path']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['container', b'container', 'path', b'path']) -> None: - ... global___MapExtractExpr = MapExtractExpr @typing_extensions.final @@ -532,19 +509,16 @@ class ConnectedExpr(google.protobuf.message.Message): This tells us whether the specified ports are connected """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor BLOCK_PORT_FIELD_NUMBER: builtins.int LINK_PORT_FIELD_NUMBER: builtins.int EXPANDED_FIELD_NUMBER: builtins.int @property - def block_port(self) -> global___ValueExpr: - ... - + def block_port(self) -> global___ValueExpr: ... @property - def link_port(self) -> global___ValueExpr: - ... - + def link_port(self) -> global___ValueExpr: ... @property def expanded(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConnectedExpr]: """During compilation, ConnectedExpr may be expanded (allocate replaced with concrete path indices, @@ -553,14 +527,23 @@ class ConnectedExpr(google.protobuf.message.Message): while the original (parent) is not modified. """ - def __init__(self, *, block_port: global___ValueExpr | None=..., link_port: global___ValueExpr | None=..., expanded: collections.abc.Iterable[global___ConnectedExpr] | None=...) -> None: - ... + def __init__( + self, + *, + block_port: global___ValueExpr | None = ..., + link_port: global___ValueExpr | None = ..., + expanded: collections.abc.Iterable[global___ConnectedExpr] | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["block_port", b"block_port", "link_port", b"link_port"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "block_port", b"block_port", "expanded", b"expanded", "link_port", b"link_port" + ], + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['block_port', b'block_port', 'link_port', b'link_port']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['block_port', b'block_port', 'expanded', b'expanded', 'link_port', b'link_port']) -> None: - ... global___ConnectedExpr = ConnectedExpr @typing_extensions.final @@ -569,31 +552,40 @@ class ExportedExpr(google.protobuf.message.Message): This tells us whether the specified port is exported to the hierarchy block exterior port """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor EXTERIOR_PORT_FIELD_NUMBER: builtins.int INTERNAL_BLOCK_PORT_FIELD_NUMBER: builtins.int EXPANDED_FIELD_NUMBER: builtins.int @property - def exterior_port(self) -> global___ValueExpr: - ... - + def exterior_port(self) -> global___ValueExpr: ... @property - def internal_block_port(self) -> global___ValueExpr: - ... - + def internal_block_port(self) -> global___ValueExpr: ... @property def expanded(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExportedExpr]: """see comment in ConnectedExpr""" - def __init__(self, *, exterior_port: global___ValueExpr | None=..., internal_block_port: global___ValueExpr | None=..., expanded: collections.abc.Iterable[global___ExportedExpr] | None=...) -> None: - ... + def __init__( + self, + *, + exterior_port: global___ValueExpr | None = ..., + internal_block_port: global___ValueExpr | None = ..., + expanded: collections.abc.Iterable[global___ExportedExpr] | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "exterior_port", b"exterior_port", "internal_block_port", b"internal_block_port" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "expanded", b"expanded", "exterior_port", b"exterior_port", "internal_block_port", b"internal_block_port" + ], + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['exterior_port', b'exterior_port', 'internal_block_port', b'internal_block_port']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['expanded', b'expanded', 'exterior_port', b'exterior_port', 'internal_block_port', b'internal_block_port']) -> None: - ... global___ExportedExpr = ExportedExpr @typing_extensions.final @@ -601,26 +593,19 @@ class AssignExpr(google.protobuf.message.Message): """Variable assignment (from an expression value), which allows dataflow to be directioned and explicit. Assignments should not be cyclic. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor DST_FIELD_NUMBER: builtins.int SRC_FIELD_NUMBER: builtins.int @property - def dst(self) -> edgir.ref_pb2.LocalPath: - ... - + def dst(self) -> edgir.ref_pb2.LocalPath: ... @property - def src(self) -> global___ValueExpr: - ... - - def __init__(self, *, dst: edgir.ref_pb2.LocalPath | None=..., src: global___ValueExpr | None=...) -> None: - ... + def src(self) -> global___ValueExpr: ... + def __init__(self, *, dst: edgir.ref_pb2.LocalPath | None = ..., src: global___ValueExpr | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["dst", b"dst", "src", b"src"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["dst", b"dst", "src", b"src"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['dst', b'dst', 'src', b'src']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['dst', b'dst', 'src', b'src']) -> None: - ... global___AssignExpr = AssignExpr @typing_extensions.final @@ -648,49 +633,29 @@ class ValueExpr(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def literal(self) -> edgir.lit_pb2.ValueLit: - ... - + def literal(self) -> edgir.lit_pb2.ValueLit: ... @property - def binary(self) -> global___BinaryExpr: - ... - + def binary(self) -> global___BinaryExpr: ... @property - def binary_set(self) -> global___BinarySetExpr: - ... - + def binary_set(self) -> global___BinarySetExpr: ... @property - def unary(self) -> global___UnaryExpr: - ... - + def unary(self) -> global___UnaryExpr: ... @property - def unary_set(self) -> global___UnarySetExpr: - ... - + def unary_set(self) -> global___UnarySetExpr: ... @property def array(self) -> global___ArrayExpr: """SetExpr set = 5;""" @property - def struct(self) -> global___StructExpr: - ... - + def struct(self) -> global___StructExpr: ... @property - def range(self) -> global___RangeExpr: - ... - + def range(self) -> global___RangeExpr: ... @property - def ifThenElse(self) -> global___IfThenElseExpr: - ... - + def ifThenElse(self) -> global___IfThenElseExpr: ... @property - def extract(self) -> global___ExtractExpr: - ... - + def extract(self) -> global___ExtractExpr: ... @property - def map_extract(self) -> global___MapExtractExpr: - ... - + def map_extract(self) -> global___MapExtractExpr: ... @property def connected(self) -> global___ConnectedExpr: """single port to single port connect""" @@ -708,9 +673,7 @@ class ValueExpr(google.protobuf.message.Message): """array to array export, where allocate means allocate a subarray""" @property - def assign(self) -> global___AssignExpr: - ... - + def assign(self) -> global___AssignExpr: ... @property def exportedTunnel(self) -> global___ExportedExpr: """These Exprs support cross-hierarchy operations @@ -732,22 +695,150 @@ class ValueExpr(google.protobuf.message.Message): """ @property - def ref(self) -> edgir.ref_pb2.LocalPath: - ... - - @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, literal: edgir.lit_pb2.ValueLit | None=..., binary: global___BinaryExpr | None=..., binary_set: global___BinarySetExpr | None=..., unary: global___UnaryExpr | None=..., unary_set: global___UnarySetExpr | None=..., array: global___ArrayExpr | None=..., struct: global___StructExpr | None=..., range: global___RangeExpr | None=..., ifThenElse: global___IfThenElseExpr | None=..., extract: global___ExtractExpr | None=..., map_extract: global___MapExtractExpr | None=..., connected: global___ConnectedExpr | None=..., exported: global___ExportedExpr | None=..., connectedArray: global___ConnectedExpr | None=..., exportedArray: global___ExportedExpr | None=..., assign: global___AssignExpr | None=..., exportedTunnel: global___ExportedExpr | None=..., assignTunnel: global___AssignExpr | None=..., ref: edgir.ref_pb2.LocalPath | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['array', b'array', 'assign', b'assign', 'assignTunnel', b'assignTunnel', 'binary', b'binary', 'binary_set', b'binary_set', 'connected', b'connected', 'connectedArray', b'connectedArray', 'exported', b'exported', 'exportedArray', b'exportedArray', 'exportedTunnel', b'exportedTunnel', 'expr', b'expr', 'extract', b'extract', 'ifThenElse', b'ifThenElse', 'literal', b'literal', 'map_extract', b'map_extract', 'meta', b'meta', 'range', b'range', 'ref', b'ref', 'struct', b'struct', 'unary', b'unary', 'unary_set', b'unary_set']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['array', b'array', 'assign', b'assign', 'assignTunnel', b'assignTunnel', 'binary', b'binary', 'binary_set', b'binary_set', 'connected', b'connected', 'connectedArray', b'connectedArray', 'exported', b'exported', 'exportedArray', b'exportedArray', 'exportedTunnel', b'exportedTunnel', 'expr', b'expr', 'extract', b'extract', 'ifThenElse', b'ifThenElse', 'literal', b'literal', 'map_extract', b'map_extract', 'meta', b'meta', 'range', b'range', 'ref', b'ref', 'struct', b'struct', 'unary', b'unary', 'unary_set', b'unary_set']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['expr', b'expr']) -> typing_extensions.Literal['literal', 'binary', 'binary_set', 'unary', 'unary_set', 'array', 'struct', 'range', 'ifThenElse', 'extract', 'map_extract', 'connected', 'exported', 'connectedArray', 'exportedArray', 'assign', 'exportedTunnel', 'assignTunnel', 'ref'] | None: - ... -global___ValueExpr = ValueExpr \ No newline at end of file + def ref(self) -> edgir.ref_pb2.LocalPath: ... + @property + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + literal: edgir.lit_pb2.ValueLit | None = ..., + binary: global___BinaryExpr | None = ..., + binary_set: global___BinarySetExpr | None = ..., + unary: global___UnaryExpr | None = ..., + unary_set: global___UnarySetExpr | None = ..., + array: global___ArrayExpr | None = ..., + struct: global___StructExpr | None = ..., + range: global___RangeExpr | None = ..., + ifThenElse: global___IfThenElseExpr | None = ..., + extract: global___ExtractExpr | None = ..., + map_extract: global___MapExtractExpr | None = ..., + connected: global___ConnectedExpr | None = ..., + exported: global___ExportedExpr | None = ..., + connectedArray: global___ConnectedExpr | None = ..., + exportedArray: global___ExportedExpr | None = ..., + assign: global___AssignExpr | None = ..., + exportedTunnel: global___ExportedExpr | None = ..., + assignTunnel: global___AssignExpr | None = ..., + ref: edgir.ref_pb2.LocalPath | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "assign", + b"assign", + "assignTunnel", + b"assignTunnel", + "binary", + b"binary", + "binary_set", + b"binary_set", + "connected", + b"connected", + "connectedArray", + b"connectedArray", + "exported", + b"exported", + "exportedArray", + b"exportedArray", + "exportedTunnel", + b"exportedTunnel", + "expr", + b"expr", + "extract", + b"extract", + "ifThenElse", + b"ifThenElse", + "literal", + b"literal", + "map_extract", + b"map_extract", + "meta", + b"meta", + "range", + b"range", + "ref", + b"ref", + "struct", + b"struct", + "unary", + b"unary", + "unary_set", + b"unary_set", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "assign", + b"assign", + "assignTunnel", + b"assignTunnel", + "binary", + b"binary", + "binary_set", + b"binary_set", + "connected", + b"connected", + "connectedArray", + b"connectedArray", + "exported", + b"exported", + "exportedArray", + b"exportedArray", + "exportedTunnel", + b"exportedTunnel", + "expr", + b"expr", + "extract", + b"extract", + "ifThenElse", + b"ifThenElse", + "literal", + b"literal", + "map_extract", + b"map_extract", + "meta", + b"meta", + "range", + b"range", + "ref", + b"ref", + "struct", + b"struct", + "unary", + b"unary", + "unary_set", + b"unary_set", + ], + ) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["expr", b"expr"]) -> ( + typing_extensions.Literal[ + "literal", + "binary", + "binary_set", + "unary", + "unary_set", + "array", + "struct", + "range", + "ifThenElse", + "extract", + "map_extract", + "connected", + "exported", + "connectedArray", + "exportedArray", + "assign", + "exportedTunnel", + "assignTunnel", + "ref", + ] + | None + ): ... + +global___ValueExpr = ValueExpr diff --git a/edg/edgir/impl_pb2.py b/edg/edgir/impl_pb2.py index 47e51e529..2392b5cbe 100644 --- a/edg/edgir/impl_pb2.py +++ b/edg/edgir/impl_pb2.py @@ -1,13 +1,18 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import common_pb2 as edgir_dot_common__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10edgir/impl.proto\x12\nedgir.impl\x1a\x12edgir/common.proto"1\n\tBlockImpl\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"0\n\x08PortImpl\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"0\n\x08LinkImpl\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"7\n\x0fEnvironmentImpl\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadatab\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x10edgir/impl.proto\x12\nedgir.impl\x1a\x12edgir/common.proto"1\n\tBlockImpl\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"0\n\x08PortImpl\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"0\n\x08LinkImpl\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata"7\n\x0fEnvironmentImpl\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadatab\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.impl_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.impl_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _BLOCKIMPL._serialized_start = 52 @@ -17,4 +22,4 @@ _LINKIMPL._serialized_start = 153 _LINKIMPL._serialized_end = 201 _ENVIRONMENTIMPL._serialized_start = 203 - _ENVIRONMENTIMPL._serialized_end = 258 \ No newline at end of file + _ENVIRONMENTIMPL._serialized_end = 258 diff --git a/edg/edgir/impl_pb2.pyi b/edg/edgir/impl_pb2.pyi index fb6492f0a..7f0b81720 100644 --- a/edg/edgir/impl_pb2.pyi +++ b/edg/edgir/impl_pb2.pyi @@ -10,11 +10,13 @@ stored. Fuck if I know what that will look like, so for now you just get a metadata block. """ + import builtins from .. import edgir import google.protobuf.descriptor import google.protobuf.message import sys + if sys.version_info >= (3, 8): import typing as typing_extensions else: @@ -27,17 +29,11 @@ class BlockImpl(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, meta: edgir.common_pb2.Metadata | None=...) -> None: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__(self, *, meta: edgir.common_pb2.Metadata | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> None: - ... global___BlockImpl = BlockImpl @typing_extensions.final @@ -46,17 +42,11 @@ class PortImpl(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, meta: edgir.common_pb2.Metadata | None=...) -> None: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__(self, *, meta: edgir.common_pb2.Metadata | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> None: - ... global___PortImpl = PortImpl @typing_extensions.final @@ -65,17 +55,11 @@ class LinkImpl(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> builtins.bool: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__(self, *, meta: edgir.common_pb2.Metadata | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> None: - ... global___LinkImpl = LinkImpl @typing_extensions.final @@ -84,15 +68,9 @@ class EnvironmentImpl(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> builtins.bool: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__(self, *, meta: edgir.common_pb2.Metadata | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> None: - ... -global___EnvironmentImpl = EnvironmentImpl \ No newline at end of file +global___EnvironmentImpl = EnvironmentImpl diff --git a/edg/edgir/init_pb2.py b/edg/edgir/init_pb2.py index 92571bac8..6b6964627 100644 --- a/edg/edgir/init_pb2.py +++ b/edg/edgir/init_pb2.py @@ -1,14 +1,19 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import common_pb2 as edgir_dot_common__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10edgir/init.proto\x12\nedgir.init\x1a\x12edgir/common.proto"\xeb\x02\n\x07ValInit\x12\'\n\x08floating\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12&\n\x07integer\x18\x02 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12&\n\x07boolean\x18\x03 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12#\n\x04text\x18\x04 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12"\n\x03set\x18\x08 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12%\n\x06struct\x18\t \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12$\n\x05range\x18\n \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12$\n\x05array\x18\x0b \x01(\x0b2\x13.edgir.init.ValInitH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.MetadataB\x05\n\x03valb\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x10edgir/init.proto\x12\nedgir.init\x1a\x12edgir/common.proto"\xeb\x02\n\x07ValInit\x12\'\n\x08floating\x18\x01 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12&\n\x07integer\x18\x02 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12&\n\x07boolean\x18\x03 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12#\n\x04text\x18\x04 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12"\n\x03set\x18\x08 \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12%\n\x06struct\x18\t \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12$\n\x05range\x18\n \x01(\x0b2\x13.edgir.common.EmptyH\x00\x12$\n\x05array\x18\x0b \x01(\x0b2\x13.edgir.init.ValInitH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.MetadataB\x05\n\x03valb\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.init_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.init_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _VALINIT._serialized_start = 53 - _VALINIT._serialized_end = 416 \ No newline at end of file + _VALINIT._serialized_end = 416 diff --git a/edg/edgir/init_pb2.pyi b/edg/edgir/init_pb2.pyi index 183e075f9..f4e1024b7 100644 --- a/edg/edgir/init_pb2.pyi +++ b/edg/edgir/init_pb2.pyi @@ -11,11 +11,13 @@ creating a value. FIXME :: Does this make more sense in another file? """ + import builtins from .. import edgir import google.protobuf.descriptor import google.protobuf.message import sys + if sys.version_info >= (3, 8): import typing as typing_extensions else: @@ -29,6 +31,7 @@ class ValInit(google.protobuf.message.Message): I think the frontend should have more type specific wrappers around this since the data required for each type can be different. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor FLOATING_FIELD_NUMBER: builtins.int INTEGER_FIELD_NUMBER: builtins.int @@ -41,50 +44,90 @@ class ValInit(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def floating(self) -> edgir.common_pb2.Empty: - ... - + def floating(self) -> edgir.common_pb2.Empty: ... @property - def integer(self) -> edgir.common_pb2.Empty: - ... - + def integer(self) -> edgir.common_pb2.Empty: ... @property - def boolean(self) -> edgir.common_pb2.Empty: - ... - + def boolean(self) -> edgir.common_pb2.Empty: ... @property - def text(self) -> edgir.common_pb2.Empty: - ... - + def text(self) -> edgir.common_pb2.Empty: ... @property - def set(self) -> edgir.common_pb2.Empty: - ... - + def set(self) -> edgir.common_pb2.Empty: ... @property - def struct(self) -> edgir.common_pb2.Empty: - ... - + def struct(self) -> edgir.common_pb2.Empty: ... @property - def range(self) -> edgir.common_pb2.Empty: - ... - + def range(self) -> edgir.common_pb2.Empty: ... @property - def array(self) -> global___ValInit: - ... - + def array(self) -> global___ValInit: ... @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, floating: edgir.common_pb2.Empty | None=..., integer: edgir.common_pb2.Empty | None=..., boolean: edgir.common_pb2.Empty | None=..., text: edgir.common_pb2.Empty | None=..., set: edgir.common_pb2.Empty | None=..., struct: edgir.common_pb2.Empty | None=..., range: edgir.common_pb2.Empty | None=..., array: global___ValInit | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['array', b'array', 'boolean', b'boolean', 'floating', b'floating', 'integer', b'integer', 'meta', b'meta', 'range', b'range', 'set', b'set', 'struct', b'struct', 'text', b'text', 'val', b'val']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['array', b'array', 'boolean', b'boolean', 'floating', b'floating', 'integer', b'integer', 'meta', b'meta', 'range', b'range', 'set', b'set', 'struct', b'struct', 'text', b'text', 'val', b'val']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['val', b'val']) -> typing_extensions.Literal['floating', 'integer', 'boolean', 'text', 'set', 'struct', 'range', 'array'] | None: - ... -global___ValInit = ValInit \ No newline at end of file + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + floating: edgir.common_pb2.Empty | None = ..., + integer: edgir.common_pb2.Empty | None = ..., + boolean: edgir.common_pb2.Empty | None = ..., + text: edgir.common_pb2.Empty | None = ..., + set: edgir.common_pb2.Empty | None = ..., + struct: edgir.common_pb2.Empty | None = ..., + range: edgir.common_pb2.Empty | None = ..., + array: global___ValInit | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "boolean", + b"boolean", + "floating", + b"floating", + "integer", + b"integer", + "meta", + b"meta", + "range", + b"range", + "set", + b"set", + "struct", + b"struct", + "text", + b"text", + "val", + b"val", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "boolean", + b"boolean", + "floating", + b"floating", + "integer", + b"integer", + "meta", + b"meta", + "range", + b"range", + "set", + b"set", + "struct", + b"struct", + "text", + b"text", + "val", + b"val", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["val", b"val"] + ) -> ( + typing_extensions.Literal["floating", "integer", "boolean", "text", "set", "struct", "range", "array"] | None + ): ... + +global___ValInit = ValInit diff --git a/edg/edgir/lit_pb2.py b/edg/edgir/lit_pb2.py index 96d84789c..5fa7a1533 100644 --- a/edg/edgir/lit_pb2.py +++ b/edg/edgir/lit_pb2.py @@ -1,17 +1,22 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import common_pb2 as edgir_dot_common__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fedgir/lit.proto\x12\tedgir.lit\x1a\x12edgir/common.proto"\x17\n\x08FloatLit\x12\x0b\n\x03val\x18\x01 \x01(\x01"\x15\n\x06IntLit\x12\x0b\n\x03val\x18\x01 \x01(\x12"\x16\n\x07BoolLit\x12\x0b\n\x03val\x18\x01 \x01(\x08"\x16\n\x07TextLit\x12\x0b\n\x03val\x18\x01 \x01(\t"V\n\x08RangeLit\x12$\n\x07minimum\x18\x01 \x01(\x0b2\x13.edgir.lit.ValueLit\x12$\n\x07maximum\x18\x02 \x01(\x0b2\x13.edgir.lit.ValueLit"\x84\x01\n\tStructLit\x122\n\x07members\x18\x01 \x03(\x0b2!.edgir.lit.StructLit.MembersEntry\x1aC\n\x0cMembersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12"\n\x05value\x18\x02 \x01(\x0b2\x13.edgir.lit.ValueLit:\x028\x01"-\n\x08ArrayLit\x12!\n\x04elts\x18\x01 \x03(\x0b2\x13.edgir.lit.ValueLit"\xc6\x02\n\x08ValueLit\x12\'\n\x08floating\x18\x01 \x01(\x0b2\x13.edgir.lit.FloatLitH\x00\x12$\n\x07integer\x18\x02 \x01(\x0b2\x11.edgir.lit.IntLitH\x00\x12%\n\x07boolean\x18\x03 \x01(\x0b2\x12.edgir.lit.BoolLitH\x00\x12"\n\x04text\x18\x04 \x01(\x0b2\x12.edgir.lit.TextLitH\x00\x12&\n\x06struct\x18\t \x01(\x0b2\x14.edgir.lit.StructLitH\x00\x12$\n\x05range\x18\n \x01(\x0b2\x13.edgir.lit.RangeLitH\x00\x12$\n\x05array\x18\x0b \x01(\x0b2\x13.edgir.lit.ArrayLitH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.MetadataB\x06\n\x04typeb\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0fedgir/lit.proto\x12\tedgir.lit\x1a\x12edgir/common.proto"\x17\n\x08FloatLit\x12\x0b\n\x03val\x18\x01 \x01(\x01"\x15\n\x06IntLit\x12\x0b\n\x03val\x18\x01 \x01(\x12"\x16\n\x07BoolLit\x12\x0b\n\x03val\x18\x01 \x01(\x08"\x16\n\x07TextLit\x12\x0b\n\x03val\x18\x01 \x01(\t"V\n\x08RangeLit\x12$\n\x07minimum\x18\x01 \x01(\x0b2\x13.edgir.lit.ValueLit\x12$\n\x07maximum\x18\x02 \x01(\x0b2\x13.edgir.lit.ValueLit"\x84\x01\n\tStructLit\x122\n\x07members\x18\x01 \x03(\x0b2!.edgir.lit.StructLit.MembersEntry\x1aC\n\x0cMembersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12"\n\x05value\x18\x02 \x01(\x0b2\x13.edgir.lit.ValueLit:\x028\x01"-\n\x08ArrayLit\x12!\n\x04elts\x18\x01 \x03(\x0b2\x13.edgir.lit.ValueLit"\xc6\x02\n\x08ValueLit\x12\'\n\x08floating\x18\x01 \x01(\x0b2\x13.edgir.lit.FloatLitH\x00\x12$\n\x07integer\x18\x02 \x01(\x0b2\x11.edgir.lit.IntLitH\x00\x12%\n\x07boolean\x18\x03 \x01(\x0b2\x12.edgir.lit.BoolLitH\x00\x12"\n\x04text\x18\x04 \x01(\x0b2\x12.edgir.lit.TextLitH\x00\x12&\n\x06struct\x18\t \x01(\x0b2\x14.edgir.lit.StructLitH\x00\x12$\n\x05range\x18\n \x01(\x0b2\x13.edgir.lit.RangeLitH\x00\x12$\n\x05array\x18\x0b \x01(\x0b2\x13.edgir.lit.ArrayLitH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.MetadataB\x06\n\x04typeb\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.lit_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.lit_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _STRUCTLIT_MEMBERSENTRY._options = None - _STRUCTLIT_MEMBERSENTRY._serialized_options = b'8\x01' + _STRUCTLIT_MEMBERSENTRY._serialized_options = b"8\x01" _FLOATLIT._serialized_start = 50 _FLOATLIT._serialized_end = 73 _INTLIT._serialized_start = 75 @@ -29,4 +34,4 @@ _ARRAYLIT._serialized_start = 369 _ARRAYLIT._serialized_end = 414 _VALUELIT._serialized_start = 417 - _VALUELIT._serialized_end = 743 \ No newline at end of file + _VALUELIT._serialized_end = 743 diff --git a/edg/edgir/lit_pb2.pyi b/edg/edgir/lit_pb2.pyi index 26f98903f..47168fcdc 100644 --- a/edg/edgir/lit_pb2.pyi +++ b/edg/edgir/lit_pb2.pyi @@ -7,6 +7,7 @@ Package : edg.lit Literals for assorted priitive types, i.e fixed constant values. """ + import builtins import collections.abc from .. import edgir @@ -14,6 +15,7 @@ import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import sys + if sys.version_info >= (3, 8): import typing as typing_extensions else: @@ -25,15 +27,14 @@ class FloatLit(google.protobuf.message.Message): """* The core expression primitives we start with are the value literals that we can use """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor VAL_FIELD_NUMBER: builtins.int val: builtins.float - def __init__(self, *, val: builtins.float=...) -> None: - ... + def __init__(self, *, val: builtins.float = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['val', b'val']) -> None: - ... global___FloatLit = FloatLit @typing_extensions.final @@ -42,11 +43,9 @@ class IntLit(google.protobuf.message.Message): VAL_FIELD_NUMBER: builtins.int val: builtins.int - def __init__(self, *, val: builtins.int=...) -> None: - ... + def __init__(self, *, val: builtins.int = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['val', b'val']) -> None: - ... global___IntLit = IntLit @typing_extensions.final @@ -55,11 +54,9 @@ class BoolLit(google.protobuf.message.Message): VAL_FIELD_NUMBER: builtins.int val: builtins.bool - def __init__(self, *, val: builtins.bool=...) -> None: - ... + def __init__(self, *, val: builtins.bool = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['val', b'val']) -> None: - ... global___BoolLit = BoolLit @typing_extensions.final @@ -68,11 +65,9 @@ class TextLit(google.protobuf.message.Message): VAL_FIELD_NUMBER: builtins.int val: builtins.str - def __init__(self, *, val: builtins.str=...) -> None: - ... + def __init__(self, *, val: builtins.str = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['val', b'val']) -> None: - ... global___TextLit = TextLit @typing_extensions.final @@ -82,21 +77,17 @@ class RangeLit(google.protobuf.message.Message): MAXIMUM_FIELD_NUMBER: builtins.int @property - def minimum(self) -> global___ValueLit: - ... - + def minimum(self) -> global___ValueLit: ... @property - def maximum(self) -> global___ValueLit: - ... - - def __init__(self, *, minimum: global___ValueLit | None=..., maximum: global___ValueLit | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['maximum', b'maximum', 'minimum', b'minimum']) -> builtins.bool: - ... + def maximum(self) -> global___ValueLit: ... + def __init__(self, *, minimum: global___ValueLit | None = ..., maximum: global___ValueLit | None = ...) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["maximum", b"maximum", "minimum", b"minimum"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["maximum", b"maximum", "minimum", b"minimum"] + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['maximum', b'maximum', 'minimum', b'minimum']) -> None: - ... global___RangeLit = RangeLit @typing_extensions.final @@ -111,28 +102,18 @@ class StructLit(google.protobuf.message.Message): key: builtins.str @property - def value(self) -> global___ValueLit: - ... + def value(self) -> global___ValueLit: ... + def __init__(self, *, key: builtins.str = ..., value: global___ValueLit | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... - def __init__(self, *, key: builtins.str=..., value: global___ValueLit | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['key', b'key', 'value', b'value']) -> None: - ... MEMBERS_FIELD_NUMBER: builtins.int @property - def members(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ValueLit]: - ... - - def __init__(self, *, members: collections.abc.Mapping[builtins.str, global___ValueLit] | None=...) -> None: - ... + def members(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ValueLit]: ... + def __init__(self, *, members: collections.abc.Mapping[builtins.str, global___ValueLit] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["members", b"members"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['members', b'members']) -> None: - ... global___StructLit = StructLit @typing_extensions.final @@ -141,14 +122,10 @@ class ArrayLit(google.protobuf.message.Message): ELTS_FIELD_NUMBER: builtins.int @property - def elts(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ValueLit]: - ... + def elts(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ValueLit]: ... + def __init__(self, *, elts: collections.abc.Iterable[global___ValueLit] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["elts", b"elts"]) -> None: ... - def __init__(self, *, elts: collections.abc.Iterable[global___ValueLit] | None=...) -> None: - ... - - def ClearField(self, field_name: typing_extensions.Literal['elts', b'elts']) -> None: - ... global___ArrayLit = ArrayLit @typing_extensions.final @@ -164,46 +141,81 @@ class ValueLit(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def floating(self) -> global___FloatLit: - ... - + def floating(self) -> global___FloatLit: ... @property - def integer(self) -> global___IntLit: - ... - + def integer(self) -> global___IntLit: ... @property - def boolean(self) -> global___BoolLit: - ... - + def boolean(self) -> global___BoolLit: ... @property - def text(self) -> global___TextLit: - ... - + def text(self) -> global___TextLit: ... @property - def struct(self) -> global___StructLit: - ... - + def struct(self) -> global___StructLit: ... @property - def range(self) -> global___RangeLit: - ... - + def range(self) -> global___RangeLit: ... @property - def array(self) -> global___ArrayLit: - ... - + def array(self) -> global___ArrayLit: ... @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, floating: global___FloatLit | None=..., integer: global___IntLit | None=..., boolean: global___BoolLit | None=..., text: global___TextLit | None=..., struct: global___StructLit | None=..., range: global___RangeLit | None=..., array: global___ArrayLit | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['array', b'array', 'boolean', b'boolean', 'floating', b'floating', 'integer', b'integer', 'meta', b'meta', 'range', b'range', 'struct', b'struct', 'text', b'text', 'type', b'type']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['array', b'array', 'boolean', b'boolean', 'floating', b'floating', 'integer', b'integer', 'meta', b'meta', 'range', b'range', 'struct', b'struct', 'text', b'text', 'type', b'type']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['type', b'type']) -> typing_extensions.Literal['floating', 'integer', 'boolean', 'text', 'struct', 'range', 'array'] | None: - ... -global___ValueLit = ValueLit \ No newline at end of file + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + floating: global___FloatLit | None = ..., + integer: global___IntLit | None = ..., + boolean: global___BoolLit | None = ..., + text: global___TextLit | None = ..., + struct: global___StructLit | None = ..., + range: global___RangeLit | None = ..., + array: global___ArrayLit | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "boolean", + b"boolean", + "floating", + b"floating", + "integer", + b"integer", + "meta", + b"meta", + "range", + b"range", + "struct", + b"struct", + "text", + b"text", + "type", + b"type", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "array", + b"array", + "boolean", + b"boolean", + "floating", + b"floating", + "integer", + b"integer", + "meta", + b"meta", + "range", + b"range", + "struct", + b"struct", + "text", + b"text", + "type", + b"type", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["type", b"type"] + ) -> typing_extensions.Literal["floating", "integer", "boolean", "text", "struct", "range", "array"] | None: ... + +global___ValueLit = ValueLit diff --git a/edg/edgir/name_pb2.py b/edg/edgir/name_pb2.py index 1b64ac05f..5d32702a7 100644 --- a/edg/edgir/name_pb2.py +++ b/edg/edgir/name_pb2.py @@ -1,16 +1,21 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import common_pb2 as edgir_dot_common__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10edgir/name.proto\x12\nedgir.name\x1a\x12edgir/common.proto"O\n\tNamespace\x12\x0f\n\x05basic\x18\x01 \x01(\tH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.MetadataB\x0b\n\tnamespace"A\n\x0bLibraryName\x12\x0c\n\x04name\x18\x02 \x01(\t\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadatab\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x10edgir/name.proto\x12\nedgir.name\x1a\x12edgir/common.proto"O\n\tNamespace\x12\x0f\n\x05basic\x18\x01 \x01(\tH\x00\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.MetadataB\x0b\n\tnamespace"A\n\x0bLibraryName\x12\x0c\n\x04name\x18\x02 \x01(\t\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadatab\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.name_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.name_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _NAMESPACE._serialized_start = 52 _NAMESPACE._serialized_end = 131 _LIBRARYNAME._serialized_start = 133 - _LIBRARYNAME._serialized_end = 198 \ No newline at end of file + _LIBRARYNAME._serialized_end = 198 diff --git a/edg/edgir/name_pb2.pyi b/edg/edgir/name_pb2.pyi index e521e90dc..6635072ee 100644 --- a/edg/edgir/name_pb2.pyi +++ b/edg/edgir/name_pb2.pyi @@ -10,11 +10,13 @@ There are three major classes of name in edg: - LocalNames, which help organize elements relative to each other. - LibraryNames, which we use to identify specific libraries. """ + import builtins from .. import edgir import google.protobuf.descriptor import google.protobuf.message import sys + if sys.version_info >= (3, 8): import typing as typing_extensions else: @@ -28,6 +30,7 @@ class Namespace(google.protobuf.message.Message): over items in the library. It lets us group elements in categories that are orthogonal to the usual Block, Port, Link, ontology. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor BASIC_FIELD_NUMBER: builtins.int META_FIELD_NUMBER: builtins.int @@ -35,20 +38,18 @@ class Namespace(google.protobuf.message.Message): "* Basic namespaces are a way to organize library elements into a\n useful hirearchy (e.g. 'Core.*' for the most primitive definitions\n that we define, or 'NXP.*' for NXP made components.)\n\n Basic namespaces should have the following properties:\n\n - First char is a capital letter\n - All other chars must be letters, numbers, '-', '<', '>'\n - CamelCase is preffered, don't use any symbols in the name\n if possible.\n " @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, basic: builtins.str=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['basic', b'basic', 'meta', b'meta', 'namespace', b'namespace']) -> builtins.bool: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__(self, *, basic: builtins.str = ..., meta: edgir.common_pb2.Metadata | None = ...) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["basic", b"basic", "meta", b"meta", "namespace", b"namespace"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["basic", b"basic", "meta", b"meta", "namespace", b"namespace"] + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["namespace", b"namespace"] + ) -> typing_extensions.Literal["basic"] | None: ... - def ClearField(self, field_name: typing_extensions.Literal['basic', b'basic', 'meta', b'meta', 'namespace', b'namespace']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['namespace', b'namespace']) -> typing_extensions.Literal['basic'] | None: - ... global___Namespace = Namespace @typing_extensions.final @@ -58,6 +59,7 @@ class LibraryName(google.protobuf.message.Message): This can be the initial element in a path or reference. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor NAME_FIELD_NUMBER: builtins.int META_FIELD_NUMBER: builtins.int @@ -65,15 +67,9 @@ class LibraryName(google.protobuf.message.Message): "* Since libraries allow for inheritance, we will often want to say\n this element, defined in *this* particular library.\n\n In those cases we want to be able to specify the relevant library\n by its identifier.\n\n Otherwise we assume it's somehow implicit which library we're\n talking about.\n " @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, name: builtins.str=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> builtins.bool: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__(self, *, name: builtins.str = ..., meta: edgir.common_pb2.Metadata | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "name", b"name"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['meta', b'meta', 'name', b'name']) -> None: - ... -global___LibraryName = LibraryName \ No newline at end of file +global___LibraryName = LibraryName diff --git a/edg/edgir/ref_pb2.py b/edg/edgir/ref_pb2.py index 154cb44fb..ee4f3fd9c 100644 --- a/edg/edgir/ref_pb2.py +++ b/edg/edgir/ref_pb2.py @@ -1,14 +1,19 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import common_pb2 as edgir_dot_common__pb2 from ..edgir import name_pb2 as edgir_dot_name__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fedgir/ref.proto\x12\tedgir.ref\x1a\x12edgir/common.proto\x1a\x10edgir/name.proto"f\n\tLocalStep\x12-\n\x0ereserved_param\x18\x01 \x01(\x0e2\x13.edgir.ref.ReservedH\x00\x12\x12\n\x08allocate\x18\x02 \x01(\tH\x00\x12\x0e\n\x04name\x18\x03 \x01(\tH\x00B\x06\n\x04step"W\n\tLocalPath\x12#\n\x05steps\x18\x01 \x03(\x0b2\x14.edgir.ref.LocalStep\x12%\n\x04meta\x18\xff\x01 \x01(\x0b2\x16.edgir.common.Metadata"\xa8\x01\n\x0bLibraryPath\x12&\n\x05start\x18\x01 \x01(\x0b2\x17.edgir.name.LibraryName\x12$\n\x05steps\x18\x02 \x03(\x0b2\x15.edgir.name.Namespace\x12$\n\x06target\x18\x03 \x01(\x0b2\x14.edgir.ref.LocalStep\x12%\n\x04meta\x18\xff\x01 \x01(\x0b2\x16.edgir.common.Metadata*r\n\x08Reserved\x12\r\n\tUNDEFINED\x10\x00\x12\x12\n\x0eCONNECTED_LINK\x10\x01\x12\x10\n\x0cIS_CONNECTED\x10(\x12\n\n\x06LENGTH\x10*\x12\x08\n\x04NAME\x10,\x12\x0c\n\x08ELEMENTS\x10-\x12\r\n\tALLOCATED\x10.b\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0fedgir/ref.proto\x12\tedgir.ref\x1a\x12edgir/common.proto\x1a\x10edgir/name.proto"f\n\tLocalStep\x12-\n\x0ereserved_param\x18\x01 \x01(\x0e2\x13.edgir.ref.ReservedH\x00\x12\x12\n\x08allocate\x18\x02 \x01(\tH\x00\x12\x0e\n\x04name\x18\x03 \x01(\tH\x00B\x06\n\x04step"W\n\tLocalPath\x12#\n\x05steps\x18\x01 \x03(\x0b2\x14.edgir.ref.LocalStep\x12%\n\x04meta\x18\xff\x01 \x01(\x0b2\x16.edgir.common.Metadata"\xa8\x01\n\x0bLibraryPath\x12&\n\x05start\x18\x01 \x01(\x0b2\x17.edgir.name.LibraryName\x12$\n\x05steps\x18\x02 \x03(\x0b2\x15.edgir.name.Namespace\x12$\n\x06target\x18\x03 \x01(\x0b2\x14.edgir.ref.LocalStep\x12%\n\x04meta\x18\xff\x01 \x01(\x0b2\x16.edgir.common.Metadata*r\n\x08Reserved\x12\r\n\tUNDEFINED\x10\x00\x12\x12\n\x0eCONNECTED_LINK\x10\x01\x12\x10\n\x0cIS_CONNECTED\x10(\x12\n\n\x06LENGTH\x10*\x12\x08\n\x04NAME\x10,\x12\x0c\n\x08ELEMENTS\x10-\x12\r\n\tALLOCATED\x10.b\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.ref_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.ref_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _RESERVED._serialized_start = 432 @@ -18,4 +23,4 @@ _LOCALPATH._serialized_start = 172 _LOCALPATH._serialized_end = 259 _LIBRARYPATH._serialized_start = 262 - _LIBRARYPATH._serialized_end = 430 \ No newline at end of file + _LIBRARYPATH._serialized_end = 430 diff --git a/edg/edgir/ref_pb2.pyi b/edg/edgir/ref_pb2.pyi index 0962a8497..477ca50ab 100644 --- a/edg/edgir/ref_pb2.pyi +++ b/edg/edgir/ref_pb2.pyi @@ -11,6 +11,7 @@ referencing different elements in a designs or libraries. We enforce certain structural properties by having a series of nested 'steps' that determine the next step in a path reference. """ + import builtins import collections.abc from .. import edgir @@ -20,6 +21,7 @@ import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import sys import typing + if sys.version_info >= (3, 10): import typing as typing_extensions else: @@ -27,41 +29,44 @@ else: DESCRIPTOR: google.protobuf.descriptor.FileDescriptor class _Reserved: - ValueType = typing.NewType('ValueType', builtins.int) + ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType -class _ReservedEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Reserved.ValueType], builtins.type): +class _ReservedEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Reserved.ValueType], builtins.type +): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor UNDEFINED: _Reserved.ValueType CONNECTED_LINK: _Reserved.ValueType - '* Directions we could move' + "* Directions we could move" IS_CONNECTED: _Reserved.ValueType - '* reserved parameters\n true implies CONNECTED_LINK resolves; not available on PortArray\n ' + "* reserved parameters\n true implies CONNECTED_LINK resolves; not available on PortArray\n " LENGTH: _Reserved.ValueType - ' EXISTS = 41;\n available on PortArray and LinkArray\n ' + " EXISTS = 41;\n available on PortArray and LinkArray\n " NAME: _Reserved.ValueType ELEMENTS: _Reserved.ValueType - 'available on PortArray and LinkArray, returns a list of string of element names' + "available on PortArray and LinkArray, returns a list of string of element names" ALLOCATED: _Reserved.ValueType - 'cannot be used as a generator dependency\n available on PortArray, returns a list of string of incoming connection names,\n ' + "cannot be used as a generator dependency\n available on PortArray, returns a list of string of incoming connection names,\n " class Reserved(_Reserved, metaclass=_ReservedEnumTypeWrapper): """* These are reserved terms that we'll end up using in various places. I'd rather have these in the block/link/bridges where they're going to exist, but that's not possible without polymorphism protibuf doesn't have """ + UNDEFINED: Reserved.ValueType CONNECTED_LINK: Reserved.ValueType -'* Directions we could move' +"* Directions we could move" IS_CONNECTED: Reserved.ValueType -'* reserved parameters\ntrue implies CONNECTED_LINK resolves; not available on PortArray\n' +"* reserved parameters\ntrue implies CONNECTED_LINK resolves; not available on PortArray\n" LENGTH: Reserved.ValueType -' EXISTS = 41;\navailable on PortArray and LinkArray\n' +" EXISTS = 41;\navailable on PortArray and LinkArray\n" NAME: Reserved.ValueType ELEMENTS: Reserved.ValueType -'available on PortArray and LinkArray, returns a list of string of element names' +"available on PortArray and LinkArray, returns a list of string of element names" ALLOCATED: Reserved.ValueType -'cannot be used as a generator dependency\navailable on PortArray, returns a list of string of incoming connection names,\n' +"cannot be used as a generator dependency\navailable on PortArray, returns a list of string of incoming connection names,\n" global___Reserved = Reserved @typing_extensions.final @@ -72,27 +77,40 @@ class LocalStep(google.protobuf.message.Message): The directions encode the type of thing we are referencing, but to the user all of these look just like local variables """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor RESERVED_PARAM_FIELD_NUMBER: builtins.int ALLOCATE_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int reserved_param: global___Reserved.ValueType allocate: builtins.str - 'Allocates a new element in an array, valid for arrays only.\n Empty string means automatically allocated, while a non-empty string is a suggested name.\n ' + "Allocates a new element in an array, valid for arrays only.\n Empty string means automatically allocated, while a non-empty string is a suggested name.\n " name: builtins.str "*\n A local name is what something is called in the context of its parent,\n whether that parent is a namespace (as in the library) or some other\n element (as in a design or heirarchy block).\n\n localNames should have the following properties:\n\n - First char is a lower case letter\n - All other chars must be letters, numbers, '-', '<', '>'\n - lowerCamelCase is preffered, don't use any symbols in the name\n if possible.\n\n These are style guidelines, literally any string will work.\n " - def __init__(self, *, reserved_param: global___Reserved.ValueType=..., allocate: builtins.str=..., name: builtins.str=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['allocate', b'allocate', 'name', b'name', 'reserved_param', b'reserved_param', 'step', b'step']) -> builtins.bool: - ... + def __init__( + self, + *, + reserved_param: global___Reserved.ValueType = ..., + allocate: builtins.str = ..., + name: builtins.str = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "allocate", b"allocate", "name", b"name", "reserved_param", b"reserved_param", "step", b"step" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "allocate", b"allocate", "name", b"name", "reserved_param", b"reserved_param", "step", b"step" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["step", b"step"] + ) -> typing_extensions.Literal["reserved_param", "allocate", "name"] | None: ... - def ClearField(self, field_name: typing_extensions.Literal['allocate', b'allocate', 'name', b'name', 'reserved_param', b'reserved_param', 'step', b'step']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['step', b'step']) -> typing_extensions.Literal['reserved_param', 'allocate', 'name'] | None: - ... global___LocalStep = LocalStep @typing_extensions.final @@ -100,26 +118,24 @@ class LocalPath(google.protobuf.message.Message): """* This is a path from a local context to some other local context. To be used as a reference. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor STEPS_FIELD_NUMBER: builtins.int META_FIELD_NUMBER: builtins.int @property - def steps(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LocalStep]: - ... - + def steps(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LocalStep]: ... @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, steps: collections.abc.Iterable[global___LocalStep] | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + steps: collections.abc.Iterable[global___LocalStep] | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "steps", b"steps"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['meta', b'meta', 'steps', b'steps']) -> None: - ... global___LocalPath = LocalPath @typing_extensions.final @@ -127,6 +143,7 @@ class LibraryPath(google.protobuf.message.Message): """* This is a path to an element within a library from the root of a library. To be used as a way to reference such elements. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor START_FIELD_NUMBER: builtins.int STEPS_FIELD_NUMBER: builtins.int @@ -134,27 +151,31 @@ class LibraryPath(google.protobuf.message.Message): META_FIELD_NUMBER: builtins.int @property - def start(self) -> edgir.name_pb2.LibraryName: - ... - + def start(self) -> edgir.name_pb2.LibraryName: ... @property - def steps(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.name_pb2.Namespace]: - ... - + def steps( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.name_pb2.Namespace]: ... @property - def target(self) -> global___LocalStep: - ... - + def target(self) -> global___LocalStep: ... @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, start: edgir.name_pb2.LibraryName | None=..., steps: collections.abc.Iterable[edgir.name_pb2.Namespace] | None=..., target: global___LocalStep | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['meta', b'meta', 'start', b'start', 'target', b'target']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['meta', b'meta', 'start', b'start', 'steps', b'steps', 'target', b'target']) -> None: - ... -global___LibraryPath = LibraryPath \ No newline at end of file + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + start: edgir.name_pb2.LibraryName | None = ..., + steps: collections.abc.Iterable[edgir.name_pb2.Namespace] | None = ..., + target: global___LocalStep | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["meta", b"meta", "start", b"start", "target", b"target"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "meta", b"meta", "start", b"start", "steps", b"steps", "target", b"target" + ], + ) -> None: ... + +global___LibraryPath = LibraryPath diff --git a/edg/edgir/schema_pb2.py b/edg/edgir/schema_pb2.py index c618687b8..a6ad0fd96 100644 --- a/edg/edgir/schema_pb2.py +++ b/edg/edgir/schema_pb2.py @@ -1,18 +1,23 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import common_pb2 as edgir_dot_common__pb2 from ..edgir import elem_pb2 as edgir_dot_elem__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12edgir/schema.proto\x12\x0cedgir.schema\x1a\x12edgir/common.proto\x1a\x10edgir/elem.proto"\x9b\x04\n\x07Library\x12*\n\x02id\x18\x01 \x01(\x0b2\x1e.edgir.schema.Library.LibIdent\x12\x0f\n\x07imports\x18\x02 \x03(\t\x12&\n\x04root\x18\n \x01(\x0b2\x18.edgir.schema.Library.NS\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata\x1a\xea\x02\n\x02NS\x126\n\x07members\x18\x01 \x03(\x0b2%.edgir.schema.Library.NS.MembersEntry\x1a\xdd\x01\n\x03Val\x12 \n\x04port\x18\n \x01(\x0b2\x10.edgir.elem.PortH\x00\x12$\n\x06bundle\x18\x0b \x01(\x0b2\x12.edgir.elem.BundleH\x00\x125\n\x0fhierarchy_block\x18\r \x01(\x0b2\x1a.edgir.elem.HierarchyBlockH\x00\x12 \n\x04link\x18\x0e \x01(\x0b2\x10.edgir.elem.LinkH\x00\x12-\n\tnamespace\x18\x14 \x01(\x0b2\x18.edgir.schema.Library.NSH\x00B\x06\n\x04type\x1aL\n\x0cMembersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b2\x1c.edgir.schema.Library.NS.Val:\x028\x01\x1a\x18\n\x08LibIdent\x12\x0c\n\x04name\x18\x01 \x01(\t"6\n\x06Design\x12,\n\x08contents\x18\x02 \x01(\x0b2\x1a.edgir.elem.HierarchyBlockb\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x12edgir/schema.proto\x12\x0cedgir.schema\x1a\x12edgir/common.proto\x1a\x10edgir/elem.proto"\x9b\x04\n\x07Library\x12*\n\x02id\x18\x01 \x01(\x0b2\x1e.edgir.schema.Library.LibIdent\x12\x0f\n\x07imports\x18\x02 \x03(\t\x12&\n\x04root\x18\n \x01(\x0b2\x18.edgir.schema.Library.NS\x12$\n\x04meta\x18\x7f \x01(\x0b2\x16.edgir.common.Metadata\x1a\xea\x02\n\x02NS\x126\n\x07members\x18\x01 \x03(\x0b2%.edgir.schema.Library.NS.MembersEntry\x1a\xdd\x01\n\x03Val\x12 \n\x04port\x18\n \x01(\x0b2\x10.edgir.elem.PortH\x00\x12$\n\x06bundle\x18\x0b \x01(\x0b2\x12.edgir.elem.BundleH\x00\x125\n\x0fhierarchy_block\x18\r \x01(\x0b2\x1a.edgir.elem.HierarchyBlockH\x00\x12 \n\x04link\x18\x0e \x01(\x0b2\x10.edgir.elem.LinkH\x00\x12-\n\tnamespace\x18\x14 \x01(\x0b2\x18.edgir.schema.Library.NSH\x00B\x06\n\x04type\x1aL\n\x0cMembersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b2\x1c.edgir.schema.Library.NS.Val:\x028\x01\x1a\x18\n\x08LibIdent\x12\x0c\n\x04name\x18\x01 \x01(\t"6\n\x06Design\x12,\n\x08contents\x18\x02 \x01(\x0b2\x1a.edgir.elem.HierarchyBlockb\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgir.schema_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgir.schema_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _LIBRARY_NS_MEMBERSENTRY._options = None - _LIBRARY_NS_MEMBERSENTRY._serialized_options = b'8\x01' + _LIBRARY_NS_MEMBERSENTRY._serialized_options = b"8\x01" _LIBRARY._serialized_start = 75 _LIBRARY._serialized_end = 614 _LIBRARY_NS._serialized_start = 226 @@ -24,4 +29,4 @@ _LIBRARY_LIBIDENT._serialized_start = 590 _LIBRARY_LIBIDENT._serialized_end = 614 _DESIGN._serialized_start = 616 - _DESIGN._serialized_end = 670 \ No newline at end of file + _DESIGN._serialized_end = 670 diff --git a/edg/edgir/schema_pb2.pyi b/edg/edgir/schema_pb2.pyi index baa53222c..d6e5e43e7 100644 --- a/edg/edgir/schema_pb2.pyi +++ b/edg/edgir/schema_pb2.pyi @@ -8,6 +8,7 @@ Package : edg.schema These types contain the highest level data structures we use to describe sets of blocks, ports, and links. """ + import builtins import collections.abc from .. import edgir @@ -15,6 +16,7 @@ import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import sys + if sys.version_info >= (3, 8): import typing as typing_extensions else: @@ -33,11 +35,13 @@ class Library(google.protobuf.message.Message): or definitional conflicts. This means that we can shuffle around partial libraries, for merging, modification, etc.. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class NS(google.protobuf.message.Message): """* Library Namespace, avoiding collision w/ edg.name.Namespace""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final @@ -50,36 +54,61 @@ class Library(google.protobuf.message.Message): NAMESPACE_FIELD_NUMBER: builtins.int @property - def port(self) -> edgir.elem_pb2.Port: - ... - + def port(self) -> edgir.elem_pb2.Port: ... @property - def bundle(self) -> edgir.elem_pb2.Bundle: - ... - + def bundle(self) -> edgir.elem_pb2.Bundle: ... @property - def hierarchy_block(self) -> edgir.elem_pb2.HierarchyBlock: - ... - + def hierarchy_block(self) -> edgir.elem_pb2.HierarchyBlock: ... @property - def link(self) -> edgir.elem_pb2.Link: - ... - + def link(self) -> edgir.elem_pb2.Link: ... @property - def namespace(self) -> global___Library.NS: - ... - - def __init__(self, *, port: edgir.elem_pb2.Port | None=..., bundle: edgir.elem_pb2.Bundle | None=..., hierarchy_block: edgir.elem_pb2.HierarchyBlock | None=..., link: edgir.elem_pb2.Link | None=..., namespace: global___Library.NS | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['bundle', b'bundle', 'hierarchy_block', b'hierarchy_block', 'link', b'link', 'namespace', b'namespace', 'port', b'port', 'type', b'type']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['bundle', b'bundle', 'hierarchy_block', b'hierarchy_block', 'link', b'link', 'namespace', b'namespace', 'port', b'port', 'type', b'type']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['type', b'type']) -> typing_extensions.Literal['port', 'bundle', 'hierarchy_block', 'link', 'namespace'] | None: - ... + def namespace(self) -> global___Library.NS: ... + def __init__( + self, + *, + port: edgir.elem_pb2.Port | None = ..., + bundle: edgir.elem_pb2.Bundle | None = ..., + hierarchy_block: edgir.elem_pb2.HierarchyBlock | None = ..., + link: edgir.elem_pb2.Link | None = ..., + namespace: global___Library.NS | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "bundle", + b"bundle", + "hierarchy_block", + b"hierarchy_block", + "link", + b"link", + "namespace", + b"namespace", + "port", + b"port", + "type", + b"type", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "bundle", + b"bundle", + "hierarchy_block", + b"hierarchy_block", + "link", + b"link", + "namespace", + b"namespace", + "port", + b"port", + "type", + b"type", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["type", b"type"] + ) -> typing_extensions.Literal["port", "bundle", "hierarchy_block", "link", "namespace"] | None: ... @typing_extensions.final class MembersEntry(google.protobuf.message.Message): @@ -89,77 +118,68 @@ class Library(google.protobuf.message.Message): key: builtins.str @property - def value(self) -> global___Library.NS.Val: - ... + def value(self) -> global___Library.NS.Val: ... + def __init__(self, *, key: builtins.str = ..., value: global___Library.NS.Val | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... - def __init__(self, *, key: builtins.str=..., value: global___Library.NS.Val | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['key', b'key', 'value', b'value']) -> None: - ... MEMBERS_FIELD_NUMBER: builtins.int @property - def members(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Library.NS.Val]: - ... - - def __init__(self, *, members: collections.abc.Mapping[builtins.str, global___Library.NS.Val] | None=...) -> None: - ... - - def ClearField(self, field_name: typing_extensions.Literal['members', b'members']) -> None: - ... + def members(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Library.NS.Val]: ... + def __init__( + self, *, members: collections.abc.Mapping[builtins.str, global___Library.NS.Val] | None = ... + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["members", b"members"]) -> None: ... @typing_extensions.final class LibIdent(google.protobuf.message.Message): """* How we identify a library within a set. Will probably evolve to capture more metadata. """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor NAME_FIELD_NUMBER: builtins.int name: builtins.str - def __init__(self, *, name: builtins.str=...) -> None: - ... + def __init__(self, *, name: builtins.str = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['name', b'name']) -> None: - ... ID_FIELD_NUMBER: builtins.int IMPORTS_FIELD_NUMBER: builtins.int ROOT_FIELD_NUMBER: builtins.int META_FIELD_NUMBER: builtins.int @property - def id(self) -> global___Library.LibIdent: - ... - + def id(self) -> global___Library.LibIdent: ... @property - def imports(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: - ... - + def imports(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... @property - def root(self) -> global___Library.NS: - ... - + def root(self) -> global___Library.NS: ... @property - def meta(self) -> edgir.common_pb2.Metadata: - ... - - def __init__(self, *, id: global___Library.LibIdent | None=..., imports: collections.abc.Iterable[builtins.str] | None=..., root: global___Library.NS | None=..., meta: edgir.common_pb2.Metadata | None=...) -> None: - ... + def meta(self) -> edgir.common_pb2.Metadata: ... + def __init__( + self, + *, + id: global___Library.LibIdent | None = ..., + imports: collections.abc.Iterable[builtins.str] | None = ..., + root: global___Library.NS | None = ..., + meta: edgir.common_pb2.Metadata | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["id", b"id", "meta", b"meta", "root", b"root"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["id", b"id", "imports", b"imports", "meta", b"meta", "root", b"root"], + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['id', b'id', 'meta', b'meta', 'root', b'root']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['id', b'id', 'imports', b'imports', 'meta', b'meta', 'root', b'root']) -> None: - ... global___Library = Library @typing_extensions.final class Design(google.protobuf.message.Message): """* This is a Design for an embedded system at some level of abstraction.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor CONTENTS_FIELD_NUMBER: builtins.int @@ -167,12 +187,8 @@ class Design(google.protobuf.message.Message): def contents(self) -> edgir.elem_pb2.HierarchyBlock: """* Delegate the actual contents of the design to a hierarchy block, for which ports are ignored""" - def __init__(self, *, contents: edgir.elem_pb2.HierarchyBlock | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['contents', b'contents']) -> builtins.bool: - ... + def __init__(self, *, contents: edgir.elem_pb2.HierarchyBlock | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["contents", b"contents"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["contents", b"contents"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['contents', b'contents']) -> None: - ... -global___Design = Design \ No newline at end of file +global___Design = Design diff --git a/edg/edgrpc/compiler_pb2.py b/edg/edgrpc/compiler_pb2.py index 235d99647..a72494fbd 100644 --- a/edg/edgrpc/compiler_pb2.py +++ b/edg/edgrpc/compiler_pb2.py @@ -1,16 +1,21 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import schema_pb2 as edgir_dot_schema__pb2 from ..edgir import ref_pb2 as edgir_dot_ref__pb2 from ..edgir import lit_pb2 as edgir_dot_lit__pb2 from ..edgrpc import hdl_pb2 as edgrpc_dot_hdl__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15edgrpc/compiler.proto\x12\x0fedgrpc.compiler\x1a\x12edgir/schema.proto\x1a\x0fedgir/ref.proto\x1a\x0fedgir/lit.proto\x1a\x10edgrpc/hdl.proto"^\n\x0bErrorRecord\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07details\x18\x04 \x01(\t"e\n\x0fCompilerRequest\x12$\n\x06design\x18\x02 \x01(\x0b2\x14.edgir.schema.Design\x12,\n\x0brefinements\x18\x03 \x01(\x0b2\x17.edgrpc.hdl.Refinements"\xf2\x01\n\x0eCompilerResult\x12$\n\x06design\x18\x01 \x01(\x0b2\x14.edgir.schema.Design\x12,\n\x06errors\x18\x04 \x03(\x0b2\x1c.edgrpc.compiler.ErrorRecord\x12;\n\x0csolvedValues\x18\x02 \x03(\x0b2%.edgrpc.compiler.CompilerResult.Value\x1aO\n\x05Value\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12"\n\x05value\x18\x02 \x01(\x0b2\x13.edgir.lit.ValueLitb\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x15edgrpc/compiler.proto\x12\x0fedgrpc.compiler\x1a\x12edgir/schema.proto\x1a\x0fedgir/ref.proto\x1a\x0fedgir/lit.proto\x1a\x10edgrpc/hdl.proto"^\n\x0bErrorRecord\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07details\x18\x04 \x01(\t"e\n\x0fCompilerRequest\x12$\n\x06design\x18\x02 \x01(\x0b2\x14.edgir.schema.Design\x12,\n\x0brefinements\x18\x03 \x01(\x0b2\x17.edgrpc.hdl.Refinements"\xf2\x01\n\x0eCompilerResult\x12$\n\x06design\x18\x01 \x01(\x0b2\x14.edgir.schema.Design\x12,\n\x06errors\x18\x04 \x03(\x0b2\x1c.edgrpc.compiler.ErrorRecord\x12;\n\x0csolvedValues\x18\x02 \x03(\x0b2%.edgrpc.compiler.CompilerResult.Value\x1aO\n\x05Value\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12"\n\x05value\x18\x02 \x01(\x0b2\x13.edgir.lit.ValueLitb\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgrpc.compiler_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgrpc.compiler_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _ERRORRECORD._serialized_start = 114 @@ -20,4 +25,4 @@ _COMPILERRESULT._serialized_start = 314 _COMPILERRESULT._serialized_end = 556 _COMPILERRESULT_VALUE._serialized_start = 477 - _COMPILERRESULT_VALUE._serialized_end = 556 \ No newline at end of file + _COMPILERRESULT_VALUE._serialized_end = 556 diff --git a/edg/edgrpc/compiler_pb2.pyi b/edg/edgrpc/compiler_pb2.pyi index e9f7f771e..162a33bd8 100644 --- a/edg/edgrpc/compiler_pb2.pyi +++ b/edg/edgrpc/compiler_pb2.pyi @@ -6,6 +6,7 @@ Interface to the HDL (eg, library fetch) is not included here. This no longer uses gRPC to avoid complexity of sockets. """ + import builtins import collections.abc from .. import edgir @@ -14,6 +15,7 @@ import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import sys + if sys.version_info >= (3, 8): import typing as typing_extensions else: @@ -32,20 +34,26 @@ class ErrorRecord(google.protobuf.message.Message): def path(self) -> edgir.ref_pb2.LocalPath: """link / block / port, cannot be the constraint""" kind: builtins.str - 'kind of error, eg failed to generate' + "kind of error, eg failed to generate" name: builtins.str - 'constraint name / short description' + "constraint name / short description" details: builtins.str - 'longer description, optional' - - def __init__(self, *, path: edgir.ref_pb2.LocalPath | None=..., kind: builtins.str=..., name: builtins.str=..., details: builtins.str=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['path', b'path']) -> builtins.bool: - ... + "longer description, optional" + + def __init__( + self, + *, + path: edgir.ref_pb2.LocalPath | None = ..., + kind: builtins.str = ..., + name: builtins.str = ..., + details: builtins.str = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["path", b"path"]) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["details", b"details", "kind", b"kind", "name", b"name", "path", b"path"], + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['details', b'details', 'kind', b'kind', 'name', b'name', 'path', b'path']) -> None: - ... global___ErrorRecord = ErrorRecord @typing_extensions.final @@ -55,21 +63,19 @@ class CompilerRequest(google.protobuf.message.Message): REFINEMENTS_FIELD_NUMBER: builtins.int @property - def design(self) -> edgir.schema_pb2.Design: - ... - + def design(self) -> edgir.schema_pb2.Design: ... @property - def refinements(self) -> edgrpc.hdl_pb2.Refinements: - ... - - def __init__(self, *, design: edgir.schema_pb2.Design | None=..., refinements: edgrpc.hdl_pb2.Refinements | None=...) -> None: - ... + def refinements(self) -> edgrpc.hdl_pb2.Refinements: ... + def __init__( + self, *, design: edgir.schema_pb2.Design | None = ..., refinements: edgrpc.hdl_pb2.Refinements | None = ... + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["design", b"design", "refinements", b"refinements"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["design", b"design", "refinements", b"refinements"] + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['design', b'design', 'refinements', b'refinements']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['design', b'design', 'refinements', b'refinements']) -> None: - ... global___CompilerRequest = CompilerRequest @typing_extensions.final @@ -83,43 +89,42 @@ class CompilerResult(google.protobuf.message.Message): VALUE_FIELD_NUMBER: builtins.int @property - def path(self) -> edgir.ref_pb2.LocalPath: - ... - + def path(self) -> edgir.ref_pb2.LocalPath: ... @property - def value(self) -> edgir.lit_pb2.ValueLit: - ... + def value(self) -> edgir.lit_pb2.ValueLit: ... + def __init__( + self, *, path: edgir.ref_pb2.LocalPath | None = ..., value: edgir.lit_pb2.ValueLit | None = ... + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["path", b"path", "value", b"value"] + ) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["path", b"path", "value", b"value"]) -> None: ... - def __init__(self, *, path: edgir.ref_pb2.LocalPath | None=..., value: edgir.lit_pb2.ValueLit | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['path', b'path', 'value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['path', b'path', 'value', b'value']) -> None: - ... DESIGN_FIELD_NUMBER: builtins.int ERRORS_FIELD_NUMBER: builtins.int SOLVEDVALUES_FIELD_NUMBER: builtins.int @property - def design(self) -> edgir.schema_pb2.Design: - ... - + def design(self) -> edgir.schema_pb2.Design: ... @property - def errors(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ErrorRecord]: - ... - + def errors(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ErrorRecord]: ... @property - def solvedValues(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CompilerResult.Value]: - ... - - def __init__(self, *, design: edgir.schema_pb2.Design | None=..., errors: collections.abc.Iterable[global___ErrorRecord] | None=..., solvedValues: collections.abc.Iterable[global___CompilerResult.Value] | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['design', b'design']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['design', b'design', 'errors', b'errors', 'solvedValues', b'solvedValues']) -> None: - ... -global___CompilerResult = CompilerResult \ No newline at end of file + def solvedValues( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CompilerResult.Value]: ... + def __init__( + self, + *, + design: edgir.schema_pb2.Design | None = ..., + errors: collections.abc.Iterable[global___ErrorRecord] | None = ..., + solvedValues: collections.abc.Iterable[global___CompilerResult.Value] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["design", b"design"]) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "design", b"design", "errors", b"errors", "solvedValues", b"solvedValues" + ], + ) -> None: ... + +global___CompilerResult = CompilerResult diff --git a/edg/edgrpc/hdl_pb2.py b/edg/edgrpc/hdl_pb2.py index 499d3c6e4..97b2a4a65 100644 --- a/edg/edgrpc/hdl_pb2.py +++ b/edg/edgrpc/hdl_pb2.py @@ -1,20 +1,25 @@ """Generated protocol buffer code.""" + from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + _sym_db = _symbol_database.Default() from ..edgir import schema_pb2 as edgir_dot_schema__pb2 from ..edgir import ref_pb2 as edgir_dot_ref__pb2 from ..edgir import elem_pb2 as edgir_dot_elem__pb2 from ..edgir import lit_pb2 as edgir_dot_lit__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10edgrpc/hdl.proto\x12\nedgrpc.hdl\x1a\x12edgir/schema.proto\x1a\x0fedgir/ref.proto\x1a\x10edgir/elem.proto\x1a\x0fedgir/lit.proto"\xb6\x04\n\x0bRefinements\x124\n\nsubclasses\x18\x01 \x03(\x0b2 .edgrpc.hdl.Refinements.Subclass\x12-\n\x06values\x18\x02 \x03(\x0b2\x1d.edgrpc.hdl.Refinements.Value\x1a\x8e\x01\n\x08Subclass\x12$\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPathH\x00\x12%\n\x03cls\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPathH\x00\x12+\n\x0breplacement\x18\x03 \x01(\x0b2\x16.edgir.ref.LibraryPathB\x08\n\x06source\x1a\xb0\x02\n\x05Value\x12$\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPathH\x00\x12A\n\tcls_param\x18\x02 \x01(\x0b2,.edgrpc.hdl.Refinements.Value.ClassParamPathH\x00\x12#\n\x04expr\x18\x03 \x01(\x0b2\x13.edgir.lit.ValueLitH\x01\x12%\n\x05param\x18\x04 \x01(\x0b2\x14.edgir.ref.LocalPathH\x01\x1a_\n\x0eClassParamPath\x12#\n\x03cls\x18\x01 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12(\n\nparam_path\x18\x02 \x01(\x0b2\x14.edgir.ref.LocalPathB\x08\n\x06sourceB\x07\n\x05value"\x1a\n\nModuleName\x12\x0c\n\x04name\x18\x01 \x01(\t"8\n\rIndexResponse\x12\'\n\x07indexed\x18\x01 \x03(\x0b2\x16.edgir.ref.LibraryPath"9\n\x0eLibraryRequest\x12\'\n\x07element\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPath"n\n\x0fLibraryResponse\x12-\n\x07element\x18\x01 \x01(\x0b2\x1c.edgir.schema.Library.NS.Val\x12,\n\x0brefinements\x18\x03 \x01(\x0b2\x17.edgrpc.hdl.Refinements"S\n\tExprValue\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12"\n\x05value\x18\x02 \x01(\x0b2\x13.edgir.lit.ValueLit"b\n\x10GeneratorRequest\x12\'\n\x07element\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12%\n\x06values\x18\x04 \x03(\x0b2\x15.edgrpc.hdl.ExprValue"B\n\x11GeneratorResponse\x12-\n\tgenerated\x18\x01 \x01(\x0b2\x1a.edgir.elem.HierarchyBlock"\x97\x01\n\x11RefinementRequest\x12/\n\x0frefinement_pass\x18\x01 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x06design\x18\x02 \x01(\x0b2\x14.edgir.schema.Design\x12+\n\x0csolvedValues\x18\x03 \x03(\x0b2\x15.edgrpc.hdl.ExprValue">\n\x12RefinementResponse\x12(\n\tnewValues\x18\x01 \x03(\x0b2\x15.edgrpc.hdl.ExprValue"\xfc\x01\n\x0eBackendRequest\x12\'\n\x07backend\x18\x01 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x06design\x18\x02 \x01(\x0b2\x14.edgir.schema.Design\x12+\n\x0csolvedValues\x18\x03 \x03(\x0b2\x15.edgrpc.hdl.ExprValue\x12<\n\targuments\x18\x04 \x03(\x0b2).edgrpc.hdl.BackendRequest.ArgumentsEntry\x1a0\n\x0eArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x028\x01"\x8e\x01\n\x0fBackendResponse\x123\n\x07results\x18\x01 \x03(\x0b2".edgrpc.hdl.BackendResponse.Result\x1aF\n\x06Result\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12\x0e\n\x04text\x18\x02 \x01(\tH\x00B\x08\n\x06result"1\n\rErrorResponse\x12\r\n\x05error\x18\x01 \x01(\t\x12\x11\n\ttraceback\x18\x02 \x01(\t"\xc8\x02\n\nHdlRequest\x12.\n\x0cindex_module\x18\x01 \x01(\x0b2\x16.edgrpc.hdl.ModuleNameH\x00\x129\n\x13get_library_element\x18\x02 \x01(\x0b2\x1a.edgrpc.hdl.LibraryRequestH\x00\x12;\n\x13elaborate_generator\x18\x03 \x01(\x0b2\x1c.edgrpc.hdl.GeneratorRequestH\x00\x127\n\x0erun_refinement\x18\x05 \x01(\x0b2\x1d.edgrpc.hdl.RefinementRequestH\x00\x121\n\x0brun_backend\x18\x04 \x01(\x0b2\x1a.edgrpc.hdl.BackendRequestH\x00\x12\x1b\n\x11get_proto_version\x18Z \x01(\rH\x00B\t\n\x07request"\xfd\x02\n\x0bHdlResponse\x121\n\x0cindex_module\x18\x01 \x01(\x0b2\x19.edgrpc.hdl.IndexResponseH\x00\x12:\n\x13get_library_element\x18\x02 \x01(\x0b2\x1b.edgrpc.hdl.LibraryResponseH\x00\x12<\n\x13elaborate_generator\x18\x03 \x01(\x0b2\x1d.edgrpc.hdl.GeneratorResponseH\x00\x128\n\x0erun_refinement\x18\x05 \x01(\x0b2\x1e.edgrpc.hdl.RefinementResponseH\x00\x122\n\x0brun_backend\x18\x04 \x01(\x0b2\x1b.edgrpc.hdl.BackendResponseH\x00\x12\x1b\n\x11get_proto_version\x18Z \x01(\rH\x00\x12*\n\x05error\x18c \x01(\x0b2\x19.edgrpc.hdl.ErrorResponseH\x00B\n\n\x08responseb\x06proto3') + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x10edgrpc/hdl.proto\x12\nedgrpc.hdl\x1a\x12edgir/schema.proto\x1a\x0fedgir/ref.proto\x1a\x10edgir/elem.proto\x1a\x0fedgir/lit.proto"\xb6\x04\n\x0bRefinements\x124\n\nsubclasses\x18\x01 \x03(\x0b2 .edgrpc.hdl.Refinements.Subclass\x12-\n\x06values\x18\x02 \x03(\x0b2\x1d.edgrpc.hdl.Refinements.Value\x1a\x8e\x01\n\x08Subclass\x12$\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPathH\x00\x12%\n\x03cls\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPathH\x00\x12+\n\x0breplacement\x18\x03 \x01(\x0b2\x16.edgir.ref.LibraryPathB\x08\n\x06source\x1a\xb0\x02\n\x05Value\x12$\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPathH\x00\x12A\n\tcls_param\x18\x02 \x01(\x0b2,.edgrpc.hdl.Refinements.Value.ClassParamPathH\x00\x12#\n\x04expr\x18\x03 \x01(\x0b2\x13.edgir.lit.ValueLitH\x01\x12%\n\x05param\x18\x04 \x01(\x0b2\x14.edgir.ref.LocalPathH\x01\x1a_\n\x0eClassParamPath\x12#\n\x03cls\x18\x01 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12(\n\nparam_path\x18\x02 \x01(\x0b2\x14.edgir.ref.LocalPathB\x08\n\x06sourceB\x07\n\x05value"\x1a\n\nModuleName\x12\x0c\n\x04name\x18\x01 \x01(\t"8\n\rIndexResponse\x12\'\n\x07indexed\x18\x01 \x03(\x0b2\x16.edgir.ref.LibraryPath"9\n\x0eLibraryRequest\x12\'\n\x07element\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPath"n\n\x0fLibraryResponse\x12-\n\x07element\x18\x01 \x01(\x0b2\x1c.edgir.schema.Library.NS.Val\x12,\n\x0brefinements\x18\x03 \x01(\x0b2\x17.edgrpc.hdl.Refinements"S\n\tExprValue\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12"\n\x05value\x18\x02 \x01(\x0b2\x13.edgir.lit.ValueLit"b\n\x10GeneratorRequest\x12\'\n\x07element\x18\x02 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12%\n\x06values\x18\x04 \x03(\x0b2\x15.edgrpc.hdl.ExprValue"B\n\x11GeneratorResponse\x12-\n\tgenerated\x18\x01 \x01(\x0b2\x1a.edgir.elem.HierarchyBlock"\x97\x01\n\x11RefinementRequest\x12/\n\x0frefinement_pass\x18\x01 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x06design\x18\x02 \x01(\x0b2\x14.edgir.schema.Design\x12+\n\x0csolvedValues\x18\x03 \x03(\x0b2\x15.edgrpc.hdl.ExprValue">\n\x12RefinementResponse\x12(\n\tnewValues\x18\x01 \x03(\x0b2\x15.edgrpc.hdl.ExprValue"\xfc\x01\n\x0eBackendRequest\x12\'\n\x07backend\x18\x01 \x01(\x0b2\x16.edgir.ref.LibraryPath\x12$\n\x06design\x18\x02 \x01(\x0b2\x14.edgir.schema.Design\x12+\n\x0csolvedValues\x18\x03 \x03(\x0b2\x15.edgrpc.hdl.ExprValue\x12<\n\targuments\x18\x04 \x03(\x0b2).edgrpc.hdl.BackendRequest.ArgumentsEntry\x1a0\n\x0eArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x028\x01"\x8e\x01\n\x0fBackendResponse\x123\n\x07results\x18\x01 \x03(\x0b2".edgrpc.hdl.BackendResponse.Result\x1aF\n\x06Result\x12"\n\x04path\x18\x01 \x01(\x0b2\x14.edgir.ref.LocalPath\x12\x0e\n\x04text\x18\x02 \x01(\tH\x00B\x08\n\x06result"1\n\rErrorResponse\x12\r\n\x05error\x18\x01 \x01(\t\x12\x11\n\ttraceback\x18\x02 \x01(\t"\xc8\x02\n\nHdlRequest\x12.\n\x0cindex_module\x18\x01 \x01(\x0b2\x16.edgrpc.hdl.ModuleNameH\x00\x129\n\x13get_library_element\x18\x02 \x01(\x0b2\x1a.edgrpc.hdl.LibraryRequestH\x00\x12;\n\x13elaborate_generator\x18\x03 \x01(\x0b2\x1c.edgrpc.hdl.GeneratorRequestH\x00\x127\n\x0erun_refinement\x18\x05 \x01(\x0b2\x1d.edgrpc.hdl.RefinementRequestH\x00\x121\n\x0brun_backend\x18\x04 \x01(\x0b2\x1a.edgrpc.hdl.BackendRequestH\x00\x12\x1b\n\x11get_proto_version\x18Z \x01(\rH\x00B\t\n\x07request"\xfd\x02\n\x0bHdlResponse\x121\n\x0cindex_module\x18\x01 \x01(\x0b2\x19.edgrpc.hdl.IndexResponseH\x00\x12:\n\x13get_library_element\x18\x02 \x01(\x0b2\x1b.edgrpc.hdl.LibraryResponseH\x00\x12<\n\x13elaborate_generator\x18\x03 \x01(\x0b2\x1d.edgrpc.hdl.GeneratorResponseH\x00\x128\n\x0erun_refinement\x18\x05 \x01(\x0b2\x1e.edgrpc.hdl.RefinementResponseH\x00\x122\n\x0brun_backend\x18\x04 \x01(\x0b2\x1b.edgrpc.hdl.BackendResponseH\x00\x12\x1b\n\x11get_proto_version\x18Z \x01(\rH\x00\x12*\n\x05error\x18c \x01(\x0b2\x19.edgrpc.hdl.ErrorResponseH\x00B\n\n\x08responseb\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'edgrpc.hdl_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "edgrpc.hdl_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _BACKENDREQUEST_ARGUMENTSENTRY._options = None - _BACKENDREQUEST_ARGUMENTSENTRY._serialized_options = b'8\x01' + _BACKENDREQUEST_ARGUMENTSENTRY._serialized_options = b"8\x01" _REFINEMENTS._serialized_start = 105 _REFINEMENTS._serialized_end = 671 _REFINEMENTS_SUBCLASS._serialized_start = 222 @@ -54,4 +59,4 @@ _HDLREQUEST._serialized_start = 1853 _HDLREQUEST._serialized_end = 2181 _HDLRESPONSE._serialized_start = 2184 - _HDLRESPONSE._serialized_end = 2565 \ No newline at end of file + _HDLRESPONSE._serialized_end = 2565 diff --git a/edg/edgrpc/hdl_pb2.pyi b/edg/edgrpc/hdl_pb2.pyi index ce0f3626c..0b1c2c9ca 100644 --- a/edg/edgrpc/hdl_pb2.pyi +++ b/edg/edgrpc/hdl_pb2.pyi @@ -4,6 +4,7 @@ isort:skip_file * Defines messages for a service provided in Python that exposes HDL-to-edgir elaboration for a compiler in a different process / language. """ + import builtins import collections.abc from .. import edgir @@ -12,6 +13,7 @@ import google.protobuf.internal.containers import google.protobuf.message import sys import typing + if sys.version_info >= (3, 8): import typing as typing_extensions else: @@ -30,28 +32,33 @@ class Refinements(google.protobuf.message.Message): REPLACEMENT_FIELD_NUMBER: builtins.int @property - def path(self) -> edgir.ref_pb2.LocalPath: - ... - + def path(self) -> edgir.ref_pb2.LocalPath: ... @property - def cls(self) -> edgir.ref_pb2.LibraryPath: - ... - + def cls(self) -> edgir.ref_pb2.LibraryPath: ... @property - def replacement(self) -> edgir.ref_pb2.LibraryPath: - ... - - def __init__(self, *, path: edgir.ref_pb2.LocalPath | None=..., cls: edgir.ref_pb2.LibraryPath | None=..., replacement: edgir.ref_pb2.LibraryPath | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['cls', b'cls', 'path', b'path', 'replacement', b'replacement', 'source', b'source']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['cls', b'cls', 'path', b'path', 'replacement', b'replacement', 'source', b'source']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['source', b'source']) -> typing_extensions.Literal['path', 'cls'] | None: - ... + def replacement(self) -> edgir.ref_pb2.LibraryPath: ... + def __init__( + self, + *, + path: edgir.ref_pb2.LocalPath | None = ..., + cls: edgir.ref_pb2.LibraryPath | None = ..., + replacement: edgir.ref_pb2.LibraryPath | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "cls", b"cls", "path", b"path", "replacement", b"replacement", "source", b"source" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "cls", b"cls", "path", b"path", "replacement", b"replacement", "source", b"source" + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["source", b"source"] + ) -> typing_extensions.Literal["path", "cls"] | None: ... @typing_extensions.final class Value(google.protobuf.message.Message): @@ -64,34 +71,28 @@ class Refinements(google.protobuf.message.Message): PARAM_PATH_FIELD_NUMBER: builtins.int @property - def cls(self) -> edgir.ref_pb2.LibraryPath: - ... - + def cls(self) -> edgir.ref_pb2.LibraryPath: ... @property - def param_path(self) -> edgir.ref_pb2.LocalPath: - ... - - def __init__(self, *, cls: edgir.ref_pb2.LibraryPath | None=..., param_path: edgir.ref_pb2.LocalPath | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['cls', b'cls', 'param_path', b'param_path']) -> builtins.bool: - ... + def param_path(self) -> edgir.ref_pb2.LocalPath: ... + def __init__( + self, *, cls: edgir.ref_pb2.LibraryPath | None = ..., param_path: edgir.ref_pb2.LocalPath | None = ... + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["cls", b"cls", "param_path", b"param_path"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["cls", b"cls", "param_path", b"param_path"] + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['cls', b'cls', 'param_path', b'param_path']) -> None: - ... PATH_FIELD_NUMBER: builtins.int CLS_PARAM_FIELD_NUMBER: builtins.int EXPR_FIELD_NUMBER: builtins.int PARAM_FIELD_NUMBER: builtins.int @property - def path(self) -> edgir.ref_pb2.LocalPath: - ... - + def path(self) -> edgir.ref_pb2.LocalPath: ... @property - def cls_param(self) -> global___Refinements.Value.ClassParamPath: - ... - + def cls_param(self) -> global___Refinements.Value.ClassParamPath: ... @property def expr(self) -> edgir.lit_pb2.ValueLit: """set to a specific value""" @@ -100,38 +101,78 @@ class Refinements(google.protobuf.message.Message): def param(self) -> edgir.ref_pb2.LocalPath: """set to a value of another parameter - invalid for classes for now""" - def __init__(self, *, path: edgir.ref_pb2.LocalPath | None=..., cls_param: global___Refinements.Value.ClassParamPath | None=..., expr: edgir.lit_pb2.ValueLit | None=..., param: edgir.ref_pb2.LocalPath | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['cls_param', b'cls_param', 'expr', b'expr', 'param', b'param', 'path', b'path', 'source', b'source', 'value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['cls_param', b'cls_param', 'expr', b'expr', 'param', b'param', 'path', b'path', 'source', b'source', 'value', b'value']) -> None: - ... - + def __init__( + self, + *, + path: edgir.ref_pb2.LocalPath | None = ..., + cls_param: global___Refinements.Value.ClassParamPath | None = ..., + expr: edgir.lit_pb2.ValueLit | None = ..., + param: edgir.ref_pb2.LocalPath | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "cls_param", + b"cls_param", + "expr", + b"expr", + "param", + b"param", + "path", + b"path", + "source", + b"source", + "value", + b"value", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "cls_param", + b"cls_param", + "expr", + b"expr", + "param", + b"param", + "path", + b"path", + "source", + b"source", + "value", + b"value", + ], + ) -> None: ... @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal['source', b'source']) -> typing_extensions.Literal['path', 'cls_param'] | None: - ... - + def WhichOneof( + self, oneof_group: typing_extensions.Literal["source", b"source"] + ) -> typing_extensions.Literal["path", "cls_param"] | None: ... @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal['value', b'value']) -> typing_extensions.Literal['expr', 'param'] | None: - ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["value", b"value"] + ) -> typing_extensions.Literal["expr", "param"] | None: ... + SUBCLASSES_FIELD_NUMBER: builtins.int VALUES_FIELD_NUMBER: builtins.int @property - def subclasses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Refinements.Subclass]: - ... - + def subclasses( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Refinements.Subclass]: ... @property - def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Refinements.Value]: - ... - - def __init__(self, *, subclasses: collections.abc.Iterable[global___Refinements.Subclass] | None=..., values: collections.abc.Iterable[global___Refinements.Value] | None=...) -> None: - ... + def values( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Refinements.Value]: ... + def __init__( + self, + *, + subclasses: collections.abc.Iterable[global___Refinements.Subclass] | None = ..., + values: collections.abc.Iterable[global___Refinements.Value] | None = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["subclasses", b"subclasses", "values", b"values"] + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['subclasses', b'subclasses', 'values', b'values']) -> None: - ... global___Refinements = Refinements @typing_extensions.final @@ -140,11 +181,9 @@ class ModuleName(google.protobuf.message.Message): NAME_FIELD_NUMBER: builtins.int name: builtins.str - def __init__(self, *, name: builtins.str=...) -> None: - ... + def __init__(self, *, name: builtins.str = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['name', b'name']) -> None: - ... global___ModuleName = ModuleName @typing_extensions.final @@ -153,14 +192,12 @@ class IndexResponse(google.protobuf.message.Message): INDEXED_FIELD_NUMBER: builtins.int @property - def indexed(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: - ... + def indexed( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[edgir.ref_pb2.LibraryPath]: ... + def __init__(self, *, indexed: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["indexed", b"indexed"]) -> None: ... - def __init__(self, *, indexed: collections.abc.Iterable[edgir.ref_pb2.LibraryPath] | None=...) -> None: - ... - - def ClearField(self, field_name: typing_extensions.Literal['indexed', b'indexed']) -> None: - ... global___IndexResponse = IndexResponse @typing_extensions.final @@ -172,14 +209,10 @@ class LibraryRequest(google.protobuf.message.Message): def element(self) -> edgir.ref_pb2.LibraryPath: """library element asked for""" - def __init__(self, *, element: edgir.ref_pb2.LibraryPath | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['element', b'element']) -> builtins.bool: - ... + def __init__(self, *, element: edgir.ref_pb2.LibraryPath | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["element", b"element"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["element", b"element"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['element', b'element']) -> None: - ... global___LibraryRequest = LibraryRequest @typing_extensions.final @@ -189,21 +222,21 @@ class LibraryResponse(google.protobuf.message.Message): REFINEMENTS_FIELD_NUMBER: builtins.int @property - def element(self) -> edgir.schema_pb2.Library.NS.Val: - ... - + def element(self) -> edgir.schema_pb2.Library.NS.Val: ... @property def refinements(self) -> global___Refinements: """only valid if element is a top-level block""" - def __init__(self, *, element: edgir.schema_pb2.Library.NS.Val | None=..., refinements: global___Refinements | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['element', b'element', 'refinements', b'refinements']) -> builtins.bool: - ... + def __init__( + self, *, element: edgir.schema_pb2.Library.NS.Val | None = ..., refinements: global___Refinements | None = ... + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["element", b"element", "refinements", b"refinements"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["element", b"element", "refinements", b"refinements"] + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['element', b'element', 'refinements', b'refinements']) -> None: - ... global___LibraryResponse = LibraryResponse @typing_extensions.final @@ -213,21 +246,15 @@ class ExprValue(google.protobuf.message.Message): VALUE_FIELD_NUMBER: builtins.int @property - def path(self) -> edgir.ref_pb2.LocalPath: - ... - + def path(self) -> edgir.ref_pb2.LocalPath: ... @property - def value(self) -> edgir.lit_pb2.ValueLit: - ... - - def __init__(self, *, path: edgir.ref_pb2.LocalPath | None=..., value: edgir.lit_pb2.ValueLit | None=...) -> None: - ... + def value(self) -> edgir.lit_pb2.ValueLit: ... + def __init__( + self, *, path: edgir.ref_pb2.LocalPath | None = ..., value: edgir.lit_pb2.ValueLit | None = ... + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["path", b"path", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["path", b"path", "value", b"value"]) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['path', b'path', 'value', b'value']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['path', b'path', 'value', b'value']) -> None: - ... global___ExprValue = ExprValue @typing_extensions.final @@ -241,17 +268,16 @@ class GeneratorRequest(google.protobuf.message.Message): """path of library element containing the generator""" @property - def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExprValue]: - ... - - def __init__(self, *, element: edgir.ref_pb2.LibraryPath | None=..., values: collections.abc.Iterable[global___ExprValue] | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['element', b'element']) -> builtins.bool: - ... + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExprValue]: ... + def __init__( + self, + *, + element: edgir.ref_pb2.LibraryPath | None = ..., + values: collections.abc.Iterable[global___ExprValue] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["element", b"element"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["element", b"element", "values", b"values"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['element', b'element', 'values', b'values']) -> None: - ... global___GeneratorRequest = GeneratorRequest @typing_extensions.final @@ -260,17 +286,11 @@ class GeneratorResponse(google.protobuf.message.Message): GENERATED_FIELD_NUMBER: builtins.int @property - def generated(self) -> edgir.elem_pb2.HierarchyBlock: - ... + def generated(self) -> edgir.elem_pb2.HierarchyBlock: ... + def __init__(self, *, generated: edgir.elem_pb2.HierarchyBlock | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["generated", b"generated"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["generated", b"generated"]) -> None: ... - def __init__(self, *, generated: edgir.elem_pb2.HierarchyBlock | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['generated', b'generated']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['generated', b'generated']) -> None: - ... global___GeneratorResponse = GeneratorResponse @typing_extensions.final @@ -278,31 +298,37 @@ class RefinementRequest(google.protobuf.message.Message): """Runs a refinement pass - something that takes a full design and solved values and generates additional values, eg for refdes assignment """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor REFINEMENT_PASS_FIELD_NUMBER: builtins.int DESIGN_FIELD_NUMBER: builtins.int SOLVEDVALUES_FIELD_NUMBER: builtins.int @property - def refinement_pass(self) -> edgir.ref_pb2.LibraryPath: - ... - + def refinement_pass(self) -> edgir.ref_pb2.LibraryPath: ... @property - def design(self) -> edgir.schema_pb2.Design: - ... - + def design(self) -> edgir.schema_pb2.Design: ... @property - def solvedValues(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExprValue]: - ... - - def __init__(self, *, refinement_pass: edgir.ref_pb2.LibraryPath | None=..., design: edgir.schema_pb2.Design | None=..., solvedValues: collections.abc.Iterable[global___ExprValue] | None=...) -> None: - ... + def solvedValues( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExprValue]: ... + def __init__( + self, + *, + refinement_pass: edgir.ref_pb2.LibraryPath | None = ..., + design: edgir.schema_pb2.Design | None = ..., + solvedValues: collections.abc.Iterable[global___ExprValue] | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["design", b"design", "refinement_pass", b"refinement_pass"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "design", b"design", "refinement_pass", b"refinement_pass", "solvedValues", b"solvedValues" + ], + ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal['design', b'design', 'refinement_pass', b'refinement_pass']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['design', b'design', 'refinement_pass', b'refinement_pass', 'solvedValues', b'solvedValues']) -> None: - ... global___RefinementRequest = RefinementRequest @typing_extensions.final @@ -311,14 +337,10 @@ class RefinementResponse(google.protobuf.message.Message): NEWVALUES_FIELD_NUMBER: builtins.int @property - def newValues(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExprValue]: - ... - - def __init__(self, *, newValues: collections.abc.Iterable[global___ExprValue] | None=...) -> None: - ... + def newValues(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExprValue]: ... + def __init__(self, *, newValues: collections.abc.Iterable[global___ExprValue] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["newValues", b"newValues"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['newValues', b'newValues']) -> None: - ... global___RefinementResponse = RefinementResponse @typing_extensions.final @@ -326,6 +348,7 @@ class BackendRequest(google.protobuf.message.Message): """Runs a backend - something that generates fabrication artifacts from a compiled design tree eg, generate KiCad netlist, or generate microcontroller firmware pinmap headers """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final @@ -336,40 +359,42 @@ class BackendRequest(google.protobuf.message.Message): key: builtins.str value: builtins.str - def __init__(self, *, key: builtins.str=..., value: builtins.str=...) -> None: - ... + def __init__(self, *, key: builtins.str = ..., value: builtins.str = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['key', b'key', 'value', b'value']) -> None: - ... BACKEND_FIELD_NUMBER: builtins.int DESIGN_FIELD_NUMBER: builtins.int SOLVEDVALUES_FIELD_NUMBER: builtins.int ARGUMENTS_FIELD_NUMBER: builtins.int @property - def backend(self) -> edgir.ref_pb2.LibraryPath: - ... - + def backend(self) -> edgir.ref_pb2.LibraryPath: ... @property - def design(self) -> edgir.schema_pb2.Design: - ... - + def design(self) -> edgir.schema_pb2.Design: ... @property - def solvedValues(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExprValue]: - ... - + def solvedValues( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExprValue]: ... @property - def arguments(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: - ... + def arguments(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + backend: edgir.ref_pb2.LibraryPath | None = ..., + design: edgir.schema_pb2.Design | None = ..., + solvedValues: collections.abc.Iterable[global___ExprValue] | None = ..., + arguments: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["backend", b"backend", "design", b"design"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "arguments", b"arguments", "backend", b"backend", "design", b"design", "solvedValues", b"solvedValues" + ], + ) -> None: ... - def __init__(self, *, backend: edgir.ref_pb2.LibraryPath | None=..., design: edgir.schema_pb2.Design | None=..., solvedValues: collections.abc.Iterable[global___ExprValue] | None=..., arguments: collections.abc.Mapping[builtins.str, builtins.str] | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['backend', b'backend', 'design', b'design']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['arguments', b'arguments', 'backend', b'backend', 'design', b'design', 'solvedValues', b'solvedValues']) -> None: - ... global___BackendRequest = BackendRequest @typing_extensions.final @@ -386,46 +411,45 @@ class BackendResponse(google.protobuf.message.Message): def path(self) -> edgir.ref_pb2.LocalPath: """path of corresponding element in design tree""" text: builtins.str - 'for now, only text supported, for KiCad netlisting' - - def __init__(self, *, path: edgir.ref_pb2.LocalPath | None=..., text: builtins.str=...) -> None: - ... + "for now, only text supported, for KiCad netlisting" + + def __init__(self, *, path: edgir.ref_pb2.LocalPath | None = ..., text: builtins.str = ...) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["path", b"path", "result", b"result", "text", b"text"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["path", b"path", "result", b"result", "text", b"text"] + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["result", b"result"] + ) -> typing_extensions.Literal["text"] | None: ... - def HasField(self, field_name: typing_extensions.Literal['path', b'path', 'result', b'result', 'text', b'text']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['path', b'path', 'result', b'result', 'text', b'text']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['result', b'result']) -> typing_extensions.Literal['text'] | None: - ... RESULTS_FIELD_NUMBER: builtins.int @property - def results(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BackendResponse.Result]: - ... + def results( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BackendResponse.Result]: ... + def __init__(self, *, results: collections.abc.Iterable[global___BackendResponse.Result] | None = ...) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["results", b"results"]) -> None: ... - def __init__(self, *, results: collections.abc.Iterable[global___BackendResponse.Result] | None=...) -> None: - ... - - def ClearField(self, field_name: typing_extensions.Literal['results', b'results']) -> None: - ... global___BackendResponse = BackendResponse @typing_extensions.final class ErrorResponse(google.protobuf.message.Message): """catch all error response""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor ERROR_FIELD_NUMBER: builtins.int TRACEBACK_FIELD_NUMBER: builtins.int error: builtins.str traceback: builtins.str - def __init__(self, *, error: builtins.str=..., traceback: builtins.str=...) -> None: - ... + def __init__(self, *, error: builtins.str = ..., traceback: builtins.str = ...) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["error", b"error", "traceback", b"traceback"] + ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal['error', b'error', 'traceback', b'traceback']) -> None: - ... global___ErrorResponse = ErrorResponse @typing_extensions.final @@ -451,26 +475,72 @@ class HdlRequest(google.protobuf.message.Message): """returns the elaborated IR""" @property - def run_refinement(self) -> global___RefinementRequest: - ... - + def run_refinement(self) -> global___RefinementRequest: ... @property - def run_backend(self) -> global___BackendRequest: - ... + def run_backend(self) -> global___BackendRequest: ... get_proto_version: builtins.int - 'no data' - - def __init__(self, *, index_module: global___ModuleName | None=..., get_library_element: global___LibraryRequest | None=..., elaborate_generator: global___GeneratorRequest | None=..., run_refinement: global___RefinementRequest | None=..., run_backend: global___BackendRequest | None=..., get_proto_version: builtins.int=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['elaborate_generator', b'elaborate_generator', 'get_library_element', b'get_library_element', 'get_proto_version', b'get_proto_version', 'index_module', b'index_module', 'request', b'request', 'run_backend', b'run_backend', 'run_refinement', b'run_refinement']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['elaborate_generator', b'elaborate_generator', 'get_library_element', b'get_library_element', 'get_proto_version', b'get_proto_version', 'index_module', b'index_module', 'request', b'request', 'run_backend', b'run_backend', 'run_refinement', b'run_refinement']) -> None: - ... + "no data" + + def __init__( + self, + *, + index_module: global___ModuleName | None = ..., + get_library_element: global___LibraryRequest | None = ..., + elaborate_generator: global___GeneratorRequest | None = ..., + run_refinement: global___RefinementRequest | None = ..., + run_backend: global___BackendRequest | None = ..., + get_proto_version: builtins.int = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "elaborate_generator", + b"elaborate_generator", + "get_library_element", + b"get_library_element", + "get_proto_version", + b"get_proto_version", + "index_module", + b"index_module", + "request", + b"request", + "run_backend", + b"run_backend", + "run_refinement", + b"run_refinement", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "elaborate_generator", + b"elaborate_generator", + "get_library_element", + b"get_library_element", + "get_proto_version", + b"get_proto_version", + "index_module", + b"index_module", + "request", + b"request", + "run_backend", + b"run_backend", + "run_refinement", + b"run_refinement", + ], + ) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["request", b"request"]) -> ( + typing_extensions.Literal[ + "index_module", + "get_library_element", + "elaborate_generator", + "run_refinement", + "run_backend", + "get_proto_version", + ] + | None + ): ... - def WhichOneof(self, oneof_group: typing_extensions.Literal['request', b'request']) -> typing_extensions.Literal['index_module', 'get_library_element', 'elaborate_generator', 'run_refinement', 'run_backend', 'get_proto_version'] | None: - ... global___HdlRequest = HdlRequest @typing_extensions.final @@ -489,35 +559,81 @@ class HdlResponse(google.protobuf.message.Message): """list of contained library elements""" @property - def get_library_element(self) -> global___LibraryResponse: - ... - + def get_library_element(self) -> global___LibraryResponse: ... @property - def elaborate_generator(self) -> global___GeneratorResponse: - ... - + def elaborate_generator(self) -> global___GeneratorResponse: ... @property - def run_refinement(self) -> global___RefinementResponse: - ... - + def run_refinement(self) -> global___RefinementResponse: ... @property - def run_backend(self) -> global___BackendResponse: - ... + def run_backend(self) -> global___BackendResponse: ... get_proto_version: builtins.int @property - def error(self) -> global___ErrorResponse: - ... - - def __init__(self, *, index_module: global___IndexResponse | None=..., get_library_element: global___LibraryResponse | None=..., elaborate_generator: global___GeneratorResponse | None=..., run_refinement: global___RefinementResponse | None=..., run_backend: global___BackendResponse | None=..., get_proto_version: builtins.int=..., error: global___ErrorResponse | None=...) -> None: - ... - - def HasField(self, field_name: typing_extensions.Literal['elaborate_generator', b'elaborate_generator', 'error', b'error', 'get_library_element', b'get_library_element', 'get_proto_version', b'get_proto_version', 'index_module', b'index_module', 'response', b'response', 'run_backend', b'run_backend', 'run_refinement', b'run_refinement']) -> builtins.bool: - ... - - def ClearField(self, field_name: typing_extensions.Literal['elaborate_generator', b'elaborate_generator', 'error', b'error', 'get_library_element', b'get_library_element', 'get_proto_version', b'get_proto_version', 'index_module', b'index_module', 'response', b'response', 'run_backend', b'run_backend', 'run_refinement', b'run_refinement']) -> None: - ... - - def WhichOneof(self, oneof_group: typing_extensions.Literal['response', b'response']) -> typing_extensions.Literal['index_module', 'get_library_element', 'elaborate_generator', 'run_refinement', 'run_backend', 'get_proto_version', 'error'] | None: - ... -global___HdlResponse = HdlResponse \ No newline at end of file + def error(self) -> global___ErrorResponse: ... + def __init__( + self, + *, + index_module: global___IndexResponse | None = ..., + get_library_element: global___LibraryResponse | None = ..., + elaborate_generator: global___GeneratorResponse | None = ..., + run_refinement: global___RefinementResponse | None = ..., + run_backend: global___BackendResponse | None = ..., + get_proto_version: builtins.int = ..., + error: global___ErrorResponse | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "elaborate_generator", + b"elaborate_generator", + "error", + b"error", + "get_library_element", + b"get_library_element", + "get_proto_version", + b"get_proto_version", + "index_module", + b"index_module", + "response", + b"response", + "run_backend", + b"run_backend", + "run_refinement", + b"run_refinement", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "elaborate_generator", + b"elaborate_generator", + "error", + b"error", + "get_library_element", + b"get_library_element", + "get_proto_version", + b"get_proto_version", + "index_module", + b"index_module", + "response", + b"response", + "run_backend", + b"run_backend", + "run_refinement", + b"run_refinement", + ], + ) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["response", b"response"]) -> ( + typing_extensions.Literal[ + "index_module", + "get_library_element", + "elaborate_generator", + "run_refinement", + "run_backend", + "get_proto_version", + "error", + ] + | None + ): ... + +global___HdlResponse = HdlResponse diff --git a/edg/electronics_model/AnalogPort.py b/edg/electronics_model/AnalogPort.py index 6faad0055..eaef510b5 100644 --- a/edg/electronics_model/AnalogPort.py +++ b/edg/electronics_model/AnalogPort.py @@ -11,221 +11,252 @@ class AnalogLink(CircuitLink): - """Analog signal, a signal that carries information by varying voltage""" - def __init__(self) -> None: - super().__init__() - - self.source = self.Port(AnalogSource()) - self.sinks = self.Port(Vector(AnalogSink())) - - self.source_impedance = self.Parameter(RangeExpr(self.source.impedance)) - self.sink_impedance = self.Parameter(RangeExpr()) - - self.voltage = self.Parameter(RangeExpr(self.source.voltage_out)) - self.signal = self.Parameter(RangeExpr(self.source.signal_out)) - self.current_drawn = self.Parameter(RangeExpr()) - - self.voltage_limits = self.Parameter(RangeExpr()) - self.signal_limits = self.Parameter(RangeExpr()) - self.current_limits = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "voltage: ", DescriptionString.FormatUnits(self.voltage, "V"), - " of limits: ", DescriptionString.FormatUnits(self.voltage_limits, "V"), - "\ncurrent: ", DescriptionString.FormatUnits(self.current_drawn, "A"), - " of limits: ", DescriptionString.FormatUnits(self.current_limits, "A"), - "\nsink impedance: ", DescriptionString.FormatUnits(self.sink_impedance, "Ω"), - ", source impedance: ", DescriptionString.FormatUnits(self.source_impedance, "Ω")) - - self.assign(self.sink_impedance, 1 / (1 / self.sinks.map_extract(lambda x: x.impedance)).sum()) - self.require(self.source.impedance.upper() <= self.sink_impedance.lower() * 0.1) # about 10x for signal integrity - self.assign(self.current_drawn, self.sinks.sum(lambda x: x.current_draw)) - - self.assign(self.voltage_limits, self.sinks.intersection(lambda x: x.voltage_limits)) - self.require(self.voltage_limits.contains(self.voltage), "incompatible voltage levels") - self.assign(self.signal_limits, self.sinks.intersection(lambda x: x.signal_limits)) - self.require(self.voltage.contains(self.signal), "signal levels not contained within voltage") - self.require(self.signal_limits.contains(self.signal), "incompatible signal levels") - self.assign(self.current_limits, self.source.current_limits) - self.require(self.current_limits.contains(self.current_drawn), "overcurrent") + """Analog signal, a signal that carries information by varying voltage""" + + def __init__(self) -> None: + super().__init__() + + self.source = self.Port(AnalogSource()) + self.sinks = self.Port(Vector(AnalogSink())) + + self.source_impedance = self.Parameter(RangeExpr(self.source.impedance)) + self.sink_impedance = self.Parameter(RangeExpr()) + + self.voltage = self.Parameter(RangeExpr(self.source.voltage_out)) + self.signal = self.Parameter(RangeExpr(self.source.signal_out)) + self.current_drawn = self.Parameter(RangeExpr()) + + self.voltage_limits = self.Parameter(RangeExpr()) + self.signal_limits = self.Parameter(RangeExpr()) + self.current_limits = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "voltage: ", + DescriptionString.FormatUnits(self.voltage, "V"), + " of limits: ", + DescriptionString.FormatUnits(self.voltage_limits, "V"), + "\ncurrent: ", + DescriptionString.FormatUnits(self.current_drawn, "A"), + " of limits: ", + DescriptionString.FormatUnits(self.current_limits, "A"), + "\nsink impedance: ", + DescriptionString.FormatUnits(self.sink_impedance, "Ω"), + ", source impedance: ", + DescriptionString.FormatUnits(self.source_impedance, "Ω"), + ) + + self.assign(self.sink_impedance, 1 / (1 / self.sinks.map_extract(lambda x: x.impedance)).sum()) + self.require( + self.source.impedance.upper() <= self.sink_impedance.lower() * 0.1 + ) # about 10x for signal integrity + self.assign(self.current_drawn, self.sinks.sum(lambda x: x.current_draw)) + + self.assign(self.voltage_limits, self.sinks.intersection(lambda x: x.voltage_limits)) + self.require(self.voltage_limits.contains(self.voltage), "incompatible voltage levels") + self.assign(self.signal_limits, self.sinks.intersection(lambda x: x.signal_limits)) + self.require(self.voltage.contains(self.signal), "signal levels not contained within voltage") + self.require(self.signal_limits.contains(self.signal), "incompatible signal levels") + self.assign(self.current_limits, self.source.current_limits) + self.require(self.current_limits.contains(self.current_drawn), "overcurrent") class AnalogBase(CircuitPort[AnalogLink]): - link_type = AnalogLink + link_type = AnalogLink - # these are here (instead of in AnalogSource) since the port may be on the other side of a bridge - def as_voltage_source(self) -> VoltageSource: - return self._convert(AnalogSourceAdapterVoltageSource()) + # these are here (instead of in AnalogSource) since the port may be on the other side of a bridge + def as_voltage_source(self) -> VoltageSource: + return self._convert(AnalogSourceAdapterVoltageSource()) class AnalogSinkBridge(CircuitPortBridge): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.outer_port = self.Port(AnalogSink(current_draw=RangeExpr(), - voltage_limits=RangeExpr(), - signal_limits=RangeExpr(), - impedance=RangeExpr())) + self.outer_port = self.Port( + AnalogSink( + current_draw=RangeExpr(), voltage_limits=RangeExpr(), signal_limits=RangeExpr(), impedance=RangeExpr() + ) + ) - # Here we ignore the current_limits of the inner port, instead relying on the main link to handle it - # The outer port's voltage_limits is untouched and should be defined in the port def. - # TODO: it's a slightly optimization to handle them here. Should it be done? - # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? - self.inner_link = self.Port(AnalogSource(voltage_out=RangeExpr(), signal_out=RangeExpr(), - current_limits=RangeExpr.ALL)) + # Here we ignore the current_limits of the inner port, instead relying on the main link to handle it + # The outer port's voltage_limits is untouched and should be defined in the port def. + # TODO: it's a slightly optimization to handle them here. Should it be done? + # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? + self.inner_link = self.Port( + AnalogSource(voltage_out=RangeExpr(), signal_out=RangeExpr(), current_limits=RangeExpr.ALL) + ) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.assign(self.outer_port.impedance, self.inner_link.link().sink_impedance) - self.assign(self.outer_port.current_draw, self.inner_link.link().current_drawn) - self.assign(self.outer_port.voltage_limits, self.inner_link.link().voltage_limits) - self.assign(self.outer_port.signal_limits, self.inner_link.link().signal_limits) + self.assign(self.outer_port.impedance, self.inner_link.link().sink_impedance) + self.assign(self.outer_port.current_draw, self.inner_link.link().current_drawn) + self.assign(self.outer_port.voltage_limits, self.inner_link.link().voltage_limits) + self.assign(self.outer_port.signal_limits, self.inner_link.link().signal_limits) - self.assign(self.inner_link.voltage_out, self.outer_port.link().voltage) - self.assign(self.inner_link.signal_out, self.outer_port.link().signal) + self.assign(self.inner_link.voltage_out, self.outer_port.link().voltage) + self.assign(self.inner_link.signal_out, self.outer_port.link().signal) class AnalogSourceBridge(CircuitPortBridge): # basic passthrough port, sources look the same inside and outside - def __init__(self) -> None: - super().__init__() - - self.outer_port = self.Port(AnalogSource(voltage_out=RangeExpr(), - signal_out=RangeExpr(), - current_limits=RangeExpr(), - impedance=RangeExpr())) - - # Here we ignore the voltage_limits of the inner port, instead relying on the main link to handle it - # The outer port's current_limits is untouched and should be defined in tte port def. - # TODO: it's a slightly optimization to handle them here. Should it be done? - # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? - self.inner_link = self.Port(AnalogSink(current_draw=RangeExpr(), - voltage_limits=RangeExpr.ALL, - signal_limits=RangeExpr.ALL, - impedance=RangeExpr())) - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.outer_port.voltage_out, self.inner_link.link().voltage) - self.assign(self.outer_port.signal_out, self.inner_link.link().signal) - self.assign(self.outer_port.impedance, self.inner_link.link().source_impedance) - self.assign(self.outer_port.current_limits, self.inner_link.link().current_limits) # TODO compensate for internal current draw - - self.assign(self.inner_link.current_draw, self.outer_port.link().current_drawn) - self.assign(self.inner_link.impedance, self.outer_port.link().sink_impedance) + def __init__(self) -> None: + super().__init__() + + self.outer_port = self.Port( + AnalogSource( + voltage_out=RangeExpr(), signal_out=RangeExpr(), current_limits=RangeExpr(), impedance=RangeExpr() + ) + ) + + # Here we ignore the voltage_limits of the inner port, instead relying on the main link to handle it + # The outer port's current_limits is untouched and should be defined in tte port def. + # TODO: it's a slightly optimization to handle them here. Should it be done? + # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? + self.inner_link = self.Port( + AnalogSink( + current_draw=RangeExpr(), + voltage_limits=RangeExpr.ALL, + signal_limits=RangeExpr.ALL, + impedance=RangeExpr(), + ) + ) + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.outer_port.voltage_out, self.inner_link.link().voltage) + self.assign(self.outer_port.signal_out, self.inner_link.link().signal) + self.assign(self.outer_port.impedance, self.inner_link.link().source_impedance) + self.assign( + self.outer_port.current_limits, self.inner_link.link().current_limits + ) # TODO compensate for internal current draw + + self.assign(self.inner_link.current_draw, self.outer_port.link().current_drawn) + self.assign(self.inner_link.impedance, self.outer_port.link().sink_impedance) class AnalogSink(AnalogBase): - bridge_type = AnalogSinkBridge - - @staticmethod - def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink], *, - voltage_limit_tolerance: Optional[RangeLike] = None, - voltage_limit_abs: Optional[RangeLike] = None, - signal_limit_tolerance: Optional[RangeLike] = None, - signal_limit_bound: Optional[Tuple[FloatLike, FloatLike]] = None, - signal_limit_abs: Optional[RangeLike] = None, - current_draw: RangeLike = RangeExpr.ZERO, - impedance: RangeLike = RangeExpr.INF) -> 'AnalogSink': - supply_range = VoltageLink._supply_voltage_range(neg, pos) - if voltage_limit_tolerance is not None: - assert voltage_limit_abs is None - voltage_limit: RangeLike = supply_range + voltage_limit_tolerance - elif voltage_limit_abs is not None: - voltage_limit = voltage_limit_abs - else: - voltage_limit = supply_range + (-0.3, 0.3) - - signal_limit: RangeLike - if signal_limit_abs is not None: - assert signal_limit_tolerance is None - assert signal_limit_bound is None - signal_limit = signal_limit_abs - elif signal_limit_tolerance is not None: - assert signal_limit_bound is None - signal_limit = supply_range + signal_limit_tolerance - elif signal_limit_bound is not None: - # signal limit bounds specified as (lower bound added to limit, upper bound added to limit) - # typically (positive, negative) - signal_limit = (supply_range.lower() + signal_limit_bound[0], - supply_range.upper() + signal_limit_bound[1]) - else: # generic default - signal_limit = supply_range - - return AnalogSink( - voltage_limits=voltage_limit, - signal_limits=signal_limit, - current_draw=current_draw, - impedance=impedance - ) - - def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, signal_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO, - impedance: RangeLike = RangeExpr.INF) -> None: - """voltage_limits are the maximum recommended voltage levels of the device (before device damage occurs), - signal_limits are for proper device functionality (e.g. non-RRIO opamps)""" - super().__init__() - self.voltage_limits = self.Parameter(RangeExpr(voltage_limits)) - self.signal_limits = self.Parameter(RangeExpr(signal_limits)) - self.current_draw = self.Parameter(RangeExpr(current_draw)) - self.impedance = self.Parameter(RangeExpr(impedance)) + bridge_type = AnalogSinkBridge + + @staticmethod + def from_supply( + neg: Port[GroundLink], + pos: Port[VoltageLink], + *, + voltage_limit_tolerance: Optional[RangeLike] = None, + voltage_limit_abs: Optional[RangeLike] = None, + signal_limit_tolerance: Optional[RangeLike] = None, + signal_limit_bound: Optional[Tuple[FloatLike, FloatLike]] = None, + signal_limit_abs: Optional[RangeLike] = None, + current_draw: RangeLike = RangeExpr.ZERO, + impedance: RangeLike = RangeExpr.INF, + ) -> "AnalogSink": + supply_range = VoltageLink._supply_voltage_range(neg, pos) + if voltage_limit_tolerance is not None: + assert voltage_limit_abs is None + voltage_limit: RangeLike = supply_range + voltage_limit_tolerance + elif voltage_limit_abs is not None: + voltage_limit = voltage_limit_abs + else: + voltage_limit = supply_range + (-0.3, 0.3) + + signal_limit: RangeLike + if signal_limit_abs is not None: + assert signal_limit_tolerance is None + assert signal_limit_bound is None + signal_limit = signal_limit_abs + elif signal_limit_tolerance is not None: + assert signal_limit_bound is None + signal_limit = supply_range + signal_limit_tolerance + elif signal_limit_bound is not None: + # signal limit bounds specified as (lower bound added to limit, upper bound added to limit) + # typically (positive, negative) + signal_limit = (supply_range.lower() + signal_limit_bound[0], supply_range.upper() + signal_limit_bound[1]) + else: # generic default + signal_limit = supply_range + + return AnalogSink( + voltage_limits=voltage_limit, signal_limits=signal_limit, current_draw=current_draw, impedance=impedance + ) + + def __init__( + self, + voltage_limits: RangeLike = RangeExpr.ALL, + signal_limits: RangeLike = RangeExpr.ALL, + current_draw: RangeLike = RangeExpr.ZERO, + impedance: RangeLike = RangeExpr.INF, + ) -> None: + """voltage_limits are the maximum recommended voltage levels of the device (before device damage occurs), + signal_limits are for proper device functionality (e.g. non-RRIO opamps)""" + super().__init__() + self.voltage_limits = self.Parameter(RangeExpr(voltage_limits)) + self.signal_limits = self.Parameter(RangeExpr(signal_limits)) + self.current_draw = self.Parameter(RangeExpr(current_draw)) + self.impedance = self.Parameter(RangeExpr(impedance)) class AnalogSourceAdapterVoltageSource(CircuitPortAdapter[VoltageSource]): - def __init__(self) -> None: - super().__init__() - self.src = self.Port(AnalogSink( # otherwise ideal - current_draw=RangeExpr() - )) - self.dst = self.Port(VoltageSource( - voltage_out=(self.src.link().voltage.upper(), self.src.link().voltage.upper()), - current_limits=(-float('inf'), float('inf')))) - self.assign(self.src.current_draw, self.dst.link().current_drawn) + def __init__(self) -> None: + super().__init__() + self.src = self.Port(AnalogSink(current_draw=RangeExpr())) # otherwise ideal + self.dst = self.Port( + VoltageSource( + voltage_out=(self.src.link().voltage.upper(), self.src.link().voltage.upper()), + current_limits=(-float("inf"), float("inf")), + ) + ) + self.assign(self.src.current_draw, self.dst.link().current_drawn) class AnalogSource(AnalogBase): - bridge_type = AnalogSourceBridge - - @staticmethod - def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink], *, - signal_out_bound: Optional[Tuple[FloatLike, FloatLike]] = None, - signal_out_abs: Optional[RangeLike] = None, - current_limits: RangeLike = RangeExpr.ALL, - impedance: RangeLike = RangeExpr.ZERO) -> 'AnalogSource': - supply_range = VoltageLink._supply_voltage_range(neg, pos) - if signal_out_bound is not None: - assert signal_out_abs is None - # signal limit bounds specified as (lower bound added to limit, upper bound added to limit) - # typically (positive, negative) - signal_out: RangeLike = (supply_range.lower() + signal_out_bound[0], - supply_range.upper() + signal_out_bound[1]) - elif signal_out_abs is not None: - assert signal_out_bound is None - signal_out = signal_out_abs - else: # generic default - signal_out = supply_range - - return AnalogSource( - voltage_out=supply_range, - signal_out=signal_out, - current_limits=current_limits, - impedance=impedance - ) - - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, signal_out: RangeLike = RangeExpr.EMPTY, - current_limits: RangeLike = RangeExpr.ALL, - impedance: RangeLike = RangeExpr.ZERO) -> None: - """voltage_out is the total voltage range the device can output (typically limited by power rails) - regardless of controls and including transients, while signal_out is the intended operating range""" - super().__init__() - self.voltage_out = self.Parameter(RangeExpr(voltage_out)) - self.signal_out = self.Parameter(RangeExpr(signal_out)) - self.current_limits = self.Parameter(RangeExpr(current_limits)) - self.impedance = self.Parameter(RangeExpr(impedance)) + bridge_type = AnalogSourceBridge + + @staticmethod + def from_supply( + neg: Port[GroundLink], + pos: Port[VoltageLink], + *, + signal_out_bound: Optional[Tuple[FloatLike, FloatLike]] = None, + signal_out_abs: Optional[RangeLike] = None, + current_limits: RangeLike = RangeExpr.ALL, + impedance: RangeLike = RangeExpr.ZERO, + ) -> "AnalogSource": + supply_range = VoltageLink._supply_voltage_range(neg, pos) + if signal_out_bound is not None: + assert signal_out_abs is None + # signal limit bounds specified as (lower bound added to limit, upper bound added to limit) + # typically (positive, negative) + signal_out: RangeLike = ( + supply_range.lower() + signal_out_bound[0], + supply_range.upper() + signal_out_bound[1], + ) + elif signal_out_abs is not None: + assert signal_out_bound is None + signal_out = signal_out_abs + else: # generic default + signal_out = supply_range + + return AnalogSource( + voltage_out=supply_range, signal_out=signal_out, current_limits=current_limits, impedance=impedance + ) + + def __init__( + self, + voltage_out: RangeLike = RangeExpr.ZERO, + signal_out: RangeLike = RangeExpr.EMPTY, + current_limits: RangeLike = RangeExpr.ALL, + impedance: RangeLike = RangeExpr.ZERO, + ) -> None: + """voltage_out is the total voltage range the device can output (typically limited by power rails) + regardless of controls and including transients, while signal_out is the intended operating range""" + super().__init__() + self.voltage_out = self.Parameter(RangeExpr(voltage_out)) + self.signal_out = self.Parameter(RangeExpr(signal_out)) + self.current_limits = self.Parameter(RangeExpr(current_limits)) + self.impedance = self.Parameter(RangeExpr(impedance)) diff --git a/edg/electronics_model/BomBackend.py b/edg/electronics_model/BomBackend.py index 1a47b125f..7c036e6ff 100644 --- a/edg/electronics_model/BomBackend.py +++ b/edg/electronics_model/BomBackend.py @@ -17,27 +17,41 @@ class BomItem(NamedTuple): part: str -class GenerateBom(BaseBackend): # creates and populates .csv file +class GenerateBom(BaseBackend): # creates and populates .csv file @override - def run(self, design: CompiledDesign, args: Dict[str, str]= {}) -> List[Tuple[edgir.LocalPath, str]]: + def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[edgir.LocalPath, str]]: assert not args bom_list = BomTransform(design).run() bom_string = io.StringIO() - csv_data = ['Id', 'Designator', 'Footprint', 'Quantity', - 'Designation', 'Supplier and Ref', 'JLCPCB Part #', - 'Manufacturer', 'Part'] # populates headers - writer = csv.writer(bom_string, lineterminator='\n', quoting=csv.QUOTE_MINIMAL) + csv_data = [ + "Id", + "Designator", + "Footprint", + "Quantity", + "Designation", + "Supplier and Ref", + "JLCPCB Part #", + "Manufacturer", + "Part", + ] # populates headers + writer = csv.writer(bom_string, lineterminator="\n", quoting=csv.QUOTE_MINIMAL) writer.writerow(csv_data) for index, (key, value) in enumerate(bom_list.items(), 1): # populates the rest of the rows - csv_data = [str(index), ','.join(bom_list[key]), key.footprint, - str(len(bom_list[key])), key.value, '', key.jlc_number, - key.manufacturer, key.part] + csv_data = [ + str(index), + ",".join(bom_list[key]), + key.footprint, + str(len(bom_list[key])), + key.value, + "", + key.jlc_number, + key.manufacturer, + key.part, + ] writer.writerow(csv_data) - return [ - (edgir.LocalPath(), bom_string.getvalue()) - ] + return [(edgir.LocalPath(), bom_string.getvalue())] class BomTransform(TransformUtil.Transform): @@ -47,18 +61,24 @@ def __init__(self, design: CompiledDesign): @override def visit_block(self, context: TransformUtil.TransformContext, block: edgir.BlockTypes) -> None: - footprint = self.design.get_value(context.path.to_tuple() + ('fp_footprint',)) - refdes = self.design.get_value(context.path.to_tuple() + ('fp_refdes',)) + footprint = self.design.get_value(context.path.to_tuple() + ("fp_footprint",)) + refdes = self.design.get_value(context.path.to_tuple() + ("fp_refdes",)) if footprint is not None and refdes is not None: - value = self.design.get_value(context.path.to_tuple() + ('fp_value',)) or '' - jlc_number = self.design.get_value(context.path.to_tuple() + ('lcsc_part',)) or '' - manufacturer = self.design.get_value(context.path.to_tuple() + ('fp_mfr',)) or '' - part = self.design.get_value(context.path.to_tuple() + ('fp_part',)) or '' - assert isinstance(footprint, str) and isinstance(refdes, str) \ - and isinstance(jlc_number, str) and isinstance(value, str) \ - and isinstance(manufacturer, str) and isinstance(part, str) - bom_item = BomItem(footprint=footprint, value=value, jlc_number=jlc_number, - manufacturer=manufacturer, part=part) + value = self.design.get_value(context.path.to_tuple() + ("fp_value",)) or "" + jlc_number = self.design.get_value(context.path.to_tuple() + ("lcsc_part",)) or "" + manufacturer = self.design.get_value(context.path.to_tuple() + ("fp_mfr",)) or "" + part = self.design.get_value(context.path.to_tuple() + ("fp_part",)) or "" + assert ( + isinstance(footprint, str) + and isinstance(refdes, str) + and isinstance(jlc_number, str) + and isinstance(value, str) + and isinstance(manufacturer, str) + and isinstance(part, str) + ) + bom_item = BomItem( + footprint=footprint, value=value, jlc_number=jlc_number, manufacturer=manufacturer, part=part + ) self.bom_list.setdefault(bom_item, []).append(refdes) def run(self) -> Dict[BomItem, List[str]]: diff --git a/edg/electronics_model/CanPort.py b/edg/electronics_model/CanPort.py index 3587a1f03..5f53ec456 100644 --- a/edg/electronics_model/CanPort.py +++ b/edg/electronics_model/CanPort.py @@ -7,106 +7,108 @@ class CanLogicLink(Link): - """Logic level CAN link, RXD and TXD signals""" - def __init__(self) -> None: - super().__init__() - self.controller = self.Port(CanControllerPort(DigitalBidir.empty())) - self.transceiver = self.Port(CanTransceiverPort(DigitalBidir.empty())) - self.passive = self.Port(Vector(CanPassivePort(DigitalBidir.empty())), optional=True) + """Logic level CAN link, RXD and TXD signals""" - # TODO write custom top level digital constraints - # TODO model frequency ... somewhere + def __init__(self) -> None: + super().__init__() + self.controller = self.Port(CanControllerPort(DigitalBidir.empty())) + self.transceiver = self.Port(CanTransceiverPort(DigitalBidir.empty())) + self.passive = self.Port(Vector(CanPassivePort(DigitalBidir.empty())), optional=True) - @override - def contents(self) -> None: - super().contents() - # TODO future: digital constraints through link inference + # TODO write custom top level digital constraints + # TODO model frequency ... somewhere - self.txd = self.connect(self.controller.txd, self.transceiver.txd, self.passive.map_extract(lambda port: port.txd), - flatten=True) - self.rxd = self.connect(self.controller.rxd, self.transceiver.rxd, self.passive.map_extract(lambda port: port.rxd), - flatten=True) + @override + def contents(self) -> None: + super().contents() + # TODO future: digital constraints through link inference + + self.txd = self.connect( + self.controller.txd, self.transceiver.txd, self.passive.map_extract(lambda port: port.txd), flatten=True + ) + self.rxd = self.connect( + self.controller.rxd, self.transceiver.rxd, self.passive.map_extract(lambda port: port.rxd), flatten=True + ) class CanControllerPort(Bundle[CanLogicLink]): - link_type = CanLogicLink + link_type = CanLogicLink - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: # ideal by default - model = DigitalBidir() - self.txd = self.Port(DigitalSource.from_bidir(model)) - self.rxd = self.Port(DigitalSink.from_bidir(model)) + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: # ideal by default + model = DigitalBidir() + self.txd = self.Port(DigitalSource.from_bidir(model)) + self.rxd = self.Port(DigitalSink.from_bidir(model)) class CanTransceiverPort(Bundle[CanLogicLink]): - link_type = CanLogicLink + link_type = CanLogicLink - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: # ideal by default - model = DigitalBidir() - self.txd = self.Port(DigitalSink.from_bidir(model)) - self.rxd = self.Port(DigitalSource.from_bidir(model)) + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: # ideal by default + model = DigitalBidir() + self.txd = self.Port(DigitalSink.from_bidir(model)) + self.rxd = self.Port(DigitalSource.from_bidir(model)) class CanPassivePort(Bundle[CanLogicLink]): - link_type = CanLogicLink + link_type = CanLogicLink - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: # ideal by default - model = DigitalBidir() - self.txd = self.Port(DigitalSink.from_bidir(model)) - self.rxd = self.Port(DigitalSink.from_bidir(model)) + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: # ideal by default + model = DigitalBidir() + self.txd = self.Port(DigitalSink.from_bidir(model)) + self.rxd = self.Port(DigitalSink.from_bidir(model)) class CanDiffLink(Link): - """Differential CAN link, CANH and CANL signals""" - def __init__(self) -> None: - super().__init__() - self.nodes = self.Port(Vector(CanDiffPort(DigitalBidir.empty()))) # TODO mark as required + """Differential CAN link, CANH and CANL signals""" + + def __init__(self) -> None: + super().__init__() + self.nodes = self.Port(Vector(CanDiffPort(DigitalBidir.empty()))) # TODO mark as required - # TODO write custom top level digital constraints - # TODO future: digital constraints through link inference + # TODO write custom top level digital constraints + # TODO future: digital constraints through link inference - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.canh = self.connect(self.nodes.map_extract(lambda node: node.canh), - flatten=True) - self.canl = self.connect(self.nodes.map_extract(lambda node: node.canl), - flatten=True) + self.canh = self.connect(self.nodes.map_extract(lambda node: node.canh), flatten=True) + self.canl = self.connect(self.nodes.map_extract(lambda node: node.canl), flatten=True) class CanDiffBridge(PortBridge): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.outer_port = self.Port(CanDiffPort(DigitalBidir.empty())) - self.inner_link = self.Port(CanDiffPort(DigitalBidir.empty())) + self.outer_port = self.Port(CanDiffPort(DigitalBidir.empty())) + self.inner_link = self.Port(CanDiffPort(DigitalBidir.empty())) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.canh_bridge = self.Block(DigitalBidirBridge()) - self.connect(self.outer_port.canh, self.canh_bridge.outer_port) - self.connect(self.canh_bridge.inner_link, self.inner_link.canh) + self.canh_bridge = self.Block(DigitalBidirBridge()) + self.connect(self.outer_port.canh, self.canh_bridge.outer_port) + self.connect(self.canh_bridge.inner_link, self.inner_link.canh) - self.canl_bridge = self.Block(DigitalBidirBridge()) - self.connect(self.outer_port.canl, self.canl_bridge.outer_port) - self.connect(self.canl_bridge.inner_link, self.inner_link.canl) + self.canl_bridge = self.Block(DigitalBidirBridge()) + self.connect(self.outer_port.canl, self.canl_bridge.outer_port) + self.connect(self.canl_bridge.inner_link, self.inner_link.canl) class CanDiffPort(Bundle[CanDiffLink]): - link_type = CanDiffLink - bridge_type = CanDiffBridge - - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: # ideal by default - model = DigitalBidir() - self.canh = self.Port(model) - self.canl = self.Port(model) + link_type = CanDiffLink + bridge_type = CanDiffBridge + + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: # ideal by default + model = DigitalBidir() + self.canh = self.Port(model) + self.canl = self.Port(model) diff --git a/edg/electronics_model/CircuitBlock.py b/edg/electronics_model/CircuitBlock.py index c0174e97f..f34c401a4 100644 --- a/edg/electronics_model/CircuitBlock.py +++ b/edg/electronics_model/CircuitBlock.py @@ -8,132 +8,154 @@ from ..core import * from ..core.HdlUserExceptions import EdgTypeError -CircuitLinkType = TypeVar('CircuitLinkType', bound=Link, covariant=True, default=Link) +CircuitLinkType = TypeVar("CircuitLinkType", bound=Link, covariant=True, default=Link) + + class CircuitPort(Port[CircuitLinkType], Generic[CircuitLinkType]): - """Electrical connection that represents a single port into a single copper net""" - pass + """Electrical connection that represents a single port into a single copper net""" + + pass + + +T = TypeVar("T", bound=BasePort) -T = TypeVar('T', bound=BasePort) class CircuitArrayReduction(Generic[T]): - def __init__(self, steps: List[Vector[Any]], port: T): - self.port = port - self.steps = steps # reduction steps + def __init__(self, steps: List[Vector[Any]], port: T): + self.port = port + self.steps = steps # reduction steps @non_library class NetBaseBlock(BaseBlock): - def net(self) -> None: - """Defines all ports on this block as copper-connected""" - self.nets = self.Metadata({'_': '_'}) # TODO should be empty + def net(self) -> None: + """Defines all ports on this block as copper-connected""" + self.nets = self.Metadata({"_": "_"}) # TODO should be empty @non_library class FootprintBlock(Block): - """Block that represents a component that has part(s) and trace(s) on the PCB. - Provides interfaces that define footprints and copper connections and generates to appropriate metadata. - """ - # TODO perhaps don't allow part / package initializers since those shouldn't be used - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.fp_footprint = self.Parameter(StringExpr()) - self.fp_pinning = self.Parameter(ArrayStringExpr()) - self.fp_datasheet = self.Parameter(StringExpr()) - - self.fp_mfr = self.Parameter(StringExpr()) - self.fp_part = self.Parameter(StringExpr()) - self.fp_value = self.Parameter(StringExpr()) - self.fp_refdes = self.Parameter(StringExpr()) - self.fp_refdes_prefix = self.Parameter(StringExpr()) - - # TODO: allow value to be taken from parameters, ideally w/ string concat from params - def footprint(self, refdes: StringLike, footprint: StringLike, pinning: Mapping[str, CircuitPort], - mfr: Optional[StringLike] = None, part: Optional[StringLike] = None, value: Optional[StringLike] = None, - datasheet: Optional[StringLike] = None) -> None: - """Creates a footprint in this circuit block. - Value is a one-line description of the part, eg 680R, 0.01uF, LPC1549, to be used as a aid during layout or - assembly""" - from ..core.Blocks import BlockElaborationState, BlockDefinitionError - from .VoltagePorts import CircuitPort - - if self._elaboration_state not in (BlockElaborationState.init, BlockElaborationState.contents, - BlockElaborationState.generate): - raise BlockDefinitionError(type(self), "can't call Footprint(...) outside __init__, contents or generate", - "call Footprint(...) inside those functions, and remember to make the super() call") - - self.fp_is_footprint = self.Metadata("") - - pinning_array = [] - for pin_name, pin_port in pinning.items(): - if not isinstance(pin_port, CircuitPort): - raise EdgTypeError(f"Footprint(...) pin", pin_port, CircuitPort) - pinning_array.append(f'{pin_name}={pin_port._name_from(self)}') - self.assign(self.fp_pinning, pinning_array) - - self.assign(self.fp_footprint, footprint) - self.assign(self.fp_refdes_prefix, refdes) - if mfr is not None: - self.assign(self.fp_mfr, mfr) - else: - self.assign(self.fp_mfr, '') - if part is not None: - self.assign(self.fp_part, part) - else: - self.assign(self.fp_part, '') - if value is not None: - self.assign(self.fp_value, value) - else: - self.assign(self.fp_value, '') - if datasheet is not None: - self.assign(self.fp_datasheet, datasheet) - else: - self.assign(self.fp_datasheet, '') + """Block that represents a component that has part(s) and trace(s) on the PCB. + Provides interfaces that define footprints and copper connections and generates to appropriate metadata. + """ + + # TODO perhaps don't allow part / package initializers since those shouldn't be used + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.fp_footprint = self.Parameter(StringExpr()) + self.fp_pinning = self.Parameter(ArrayStringExpr()) + self.fp_datasheet = self.Parameter(StringExpr()) + + self.fp_mfr = self.Parameter(StringExpr()) + self.fp_part = self.Parameter(StringExpr()) + self.fp_value = self.Parameter(StringExpr()) + self.fp_refdes = self.Parameter(StringExpr()) + self.fp_refdes_prefix = self.Parameter(StringExpr()) + + # TODO: allow value to be taken from parameters, ideally w/ string concat from params + def footprint( + self, + refdes: StringLike, + footprint: StringLike, + pinning: Mapping[str, CircuitPort], + mfr: Optional[StringLike] = None, + part: Optional[StringLike] = None, + value: Optional[StringLike] = None, + datasheet: Optional[StringLike] = None, + ) -> None: + """Creates a footprint in this circuit block. + Value is a one-line description of the part, eg 680R, 0.01uF, LPC1549, to be used as a aid during layout or + assembly""" + from ..core.Blocks import BlockElaborationState, BlockDefinitionError + from .VoltagePorts import CircuitPort + + if self._elaboration_state not in ( + BlockElaborationState.init, + BlockElaborationState.contents, + BlockElaborationState.generate, + ): + raise BlockDefinitionError( + type(self), + "can't call Footprint(...) outside __init__, contents or generate", + "call Footprint(...) inside those functions, and remember to make the super() call", + ) + + self.fp_is_footprint = self.Metadata("") + + pinning_array = [] + for pin_name, pin_port in pinning.items(): + if not isinstance(pin_port, CircuitPort): + raise EdgTypeError(f"Footprint(...) pin", pin_port, CircuitPort) + pinning_array.append(f"{pin_name}={pin_port._name_from(self)}") + self.assign(self.fp_pinning, pinning_array) + + self.assign(self.fp_footprint, footprint) + self.assign(self.fp_refdes_prefix, refdes) + if mfr is not None: + self.assign(self.fp_mfr, mfr) + else: + self.assign(self.fp_mfr, "") + if part is not None: + self.assign(self.fp_part, part) + else: + self.assign(self.fp_part, "") + if value is not None: + self.assign(self.fp_value, value) + else: + self.assign(self.fp_value, "") + if datasheet is not None: + self.assign(self.fp_datasheet, datasheet) + else: + self.assign(self.fp_datasheet, "") @non_library class WrapperFootprintBlock(FootprintBlock): - """Block that has a footprint and optional internal contents, but the netlister ignores internal components. - Useful for, for example, a breakout board where the modelling details are provided by internal chip blocks, - but needs to show up as only a carrier board footprint. - EXPERIMENTAL - API SUBJECT TO CHANGE.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.fp_is_wrapper = self.Metadata("A") # TODO replace with not metadata, eg superclass inspection + """Block that has a footprint and optional internal contents, but the netlister ignores internal components. + Useful for, for example, a breakout board where the modelling details are provided by internal chip blocks, + but needs to show up as only a carrier board footprint. + EXPERIMENTAL - API SUBJECT TO CHANGE.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.fp_is_wrapper = self.Metadata("A") # TODO replace with not metadata, eg superclass inspection @abstract_block class NetBlock(InternalBlock, NetBaseBlock, Block): - @override - def contents(self) -> None: - super().contents() - self.net() + @override + def contents(self) -> None: + super().contents() + self.net() @abstract_block class CircuitPortBridge(NetBaseBlock, PortBridge): - @override - def contents(self) -> None: - super().contents() - self.net() + @override + def contents(self) -> None: + super().contents() + self.net() + + +AdapterDstType = TypeVar("AdapterDstType", covariant=True, bound="CircuitPort", default="CircuitPort") -AdapterDstType = TypeVar('AdapterDstType', covariant=True, bound='CircuitPort', default='CircuitPort') @abstract_block class CircuitPortAdapter(KiCadImportableBlock, NetBaseBlock, PortAdapter[AdapterDstType], Generic[AdapterDstType]): - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'edg_importable:Adapter' - return {'1': self.src, '2': self.dst} + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name == "edg_importable:Adapter" + return {"1": self.src, "2": self.dst} - @override - def contents(self) -> None: - super().contents() - self.net() + @override + def contents(self) -> None: + super().contents() + self.net() @non_library # TODO make abstract instead? class CircuitLink(NetBaseBlock, Link): - @override - def contents(self) -> None: - super().contents() - self.net() + @override + def contents(self) -> None: + super().contents() + self.net() diff --git a/edg/electronics_model/CircuitPackingBlock.py b/edg/electronics_model/CircuitPackingBlock.py index 822cccc1d..b7606559c 100644 --- a/edg/electronics_model/CircuitPackingBlock.py +++ b/edg/electronics_model/CircuitPackingBlock.py @@ -10,76 +10,75 @@ @abstract_block class NetPackingBlock(InternalBlock, Block): - def packed(self, elts: BasePort, merged: BasePort) -> None: - """Asserts that elts are all connected to the same net, and connects them to merged.""" - self.nets_packed = self.Metadata({ - 'src': self._ports.name_of(elts), - 'dst': self._ports.name_of(merged) - }) + def packed(self, elts: BasePort, merged: BasePort) -> None: + """Asserts that elts are all connected to the same net, and connects them to merged.""" + self.nets_packed = self.Metadata({"src": self._ports.name_of(elts), "dst": self._ports.name_of(merged)}) class PackedPassive(NetPackingBlock, GeneratorBlock): - def __init__(self) -> None: - super().__init__() - self.elts = self.Port(Vector(Passive.empty())) - self.merged = self.Port(Passive.empty()) - self.generator_param(self.elts.requested()) - self.packed(self.elts, self.merged) + def __init__(self) -> None: + super().__init__() + self.elts = self.Port(Vector(Passive.empty())) + self.merged = self.Port(Passive.empty()) + self.generator_param(self.elts.requested()) + self.packed(self.elts, self.merged) - @override - def generate(self) -> None: - super().generate() - self.elts.defined() - for request in self.get(self.elts.requested()): - self.elts.append_elt(Passive(), request) + @override + def generate(self) -> None: + super().generate() + self.elts.defined() + for request in self.get(self.elts.requested()): + self.elts.append_elt(Passive(), request) class PackedGround(NetPackingBlock, GeneratorBlock): - """Takes in several Ground connections that are of the same net (asserted in netlister), - and provides a single Ground.""" - def __init__(self) -> None: - super().__init__() - self.gnd_ins = self.Port(Vector(Ground.empty())) - self.gnd_out = self.Port(GroundReference( - voltage_out=RangeExpr(), - )) - self.generator_param(self.gnd_ins.requested()) - self.packed(self.gnd_ins, self.gnd_out) - - @override - def generate(self) -> None: - super().generate() - self.gnd_ins.defined() - for in_request in self.get(self.gnd_ins.requested()): - self.gnd_ins.append_elt(Ground(), in_request) - - self.assign(self.gnd_out.voltage_out, - self.gnd_ins.hull(lambda x: x.link().voltage)) + """Takes in several Ground connections that are of the same net (asserted in netlister), + and provides a single Ground.""" + + def __init__(self) -> None: + super().__init__() + self.gnd_ins = self.Port(Vector(Ground.empty())) + self.gnd_out = self.Port( + GroundReference( + voltage_out=RangeExpr(), + ) + ) + self.generator_param(self.gnd_ins.requested()) + self.packed(self.gnd_ins, self.gnd_out) + + @override + def generate(self) -> None: + super().generate() + self.gnd_ins.defined() + for in_request in self.get(self.gnd_ins.requested()): + self.gnd_ins.append_elt(Ground(), in_request) + + self.assign(self.gnd_out.voltage_out, self.gnd_ins.hull(lambda x: x.link().voltage)) class PackedVoltageSource(NetPackingBlock, GeneratorBlock): - """Takes in several VoltageSink connections that are of the same net (asserted in netlister), - and provides a single VoltageSource. Distributes the current draw from the VoltageSource - equally among the inputs.""" - def __init__(self) -> None: - super().__init__() - self.pwr_ins = self.Port(Vector(VoltageSink.empty())) - self.pwr_out = self.Port(VoltageSource( - voltage_out=RangeExpr(), - current_limits=RangeExpr.ALL - )) - self.generator_param(self.pwr_ins.requested()) - self.packed(self.pwr_ins, self.pwr_out) - - @override - def generate(self) -> None: - super().generate() - self.pwr_ins.defined() - for in_request in self.get(self.pwr_ins.requested()): - self.pwr_ins.append_elt(VoltageSink( - voltage_limits=RangeExpr.ALL, - current_draw=self.pwr_out.link().current_drawn / len(self.get(self.pwr_ins.requested())) - ), in_request) - - self.assign(self.pwr_out.voltage_out, - self.pwr_ins.hull(lambda x: x.link().voltage)) + """Takes in several VoltageSink connections that are of the same net (asserted in netlister), + and provides a single VoltageSource. Distributes the current draw from the VoltageSource + equally among the inputs.""" + + def __init__(self) -> None: + super().__init__() + self.pwr_ins = self.Port(Vector(VoltageSink.empty())) + self.pwr_out = self.Port(VoltageSource(voltage_out=RangeExpr(), current_limits=RangeExpr.ALL)) + self.generator_param(self.pwr_ins.requested()) + self.packed(self.pwr_ins, self.pwr_out) + + @override + def generate(self) -> None: + super().generate() + self.pwr_ins.defined() + for in_request in self.get(self.pwr_ins.requested()): + self.pwr_ins.append_elt( + VoltageSink( + voltage_limits=RangeExpr.ALL, + current_draw=self.pwr_out.link().current_drawn / len(self.get(self.pwr_ins.requested())), + ), + in_request, + ) + + self.assign(self.pwr_out.voltage_out, self.pwr_ins.hull(lambda x: x.link().voltage)) diff --git a/edg/electronics_model/ConnectedGenerator.py b/edg/electronics_model/ConnectedGenerator.py index 573f9aff7..26a7f7a5e 100644 --- a/edg/electronics_model/ConnectedGenerator.py +++ b/edg/electronics_model/ConnectedGenerator.py @@ -8,62 +8,67 @@ @abstract_block # really this is just a category class DefaultConnectionBlock(InternalBlock): - """A utility block that takes in an input port that may be connected - and an output port that is required, and if the input is not present, connects the - output to a default port. - If the input is present, the default port presents an 'ideal' port. - TODO can this be a true disconnect? - If the input is not present, the default port is connected to the output. - """ - pass - - -OutputType = TypeVar('OutputType', covariant=True, bound=Port, default=Port) -InputsType = TypeVar('InputsType', covariant=True, bound=Port, default=Port) -LinkType = TypeVar('LinkType', covariant=True, bound=Link, default=Link) -SelfType = TypeVar('SelfType', bound='BaseConnectedGenerator') + """A utility block that takes in an input port that may be connected + and an output port that is required, and if the input is not present, connects the + output to a default port. + If the input is present, the default port presents an 'ideal' port. - TODO can this be a true disconnect? + If the input is not present, the default port is connected to the output. + """ + + pass + + +OutputType = TypeVar("OutputType", covariant=True, bound=Port, default=Port) +InputsType = TypeVar("InputsType", covariant=True, bound=Port, default=Port) +LinkType = TypeVar("LinkType", covariant=True, bound=Link, default=Link) +SelfType = TypeVar("SelfType", bound="BaseConnectedGenerator") + + @non_library # this can't be instantiated class BaseConnectedGenerator(DefaultConnectionBlock, GeneratorBlock, Generic[OutputType, InputsType, LinkType]): - """The template actually lives here""" - INPUTS_TYPE: Type[Port] - OUTPUT_TYPE: Type[Port] - - def __init__(self, in_is_connected: BoolLike = BoolExpr()) -> None: - """in_is_connected needs to be connected from above, since from the perspective - of this block, the input is always (locally) connected""" - super().__init__() - self.out = self.Port(self.OUTPUT_TYPE.empty()) - self.in_connected = self.Port(self.INPUTS_TYPE.empty(), optional=True) - self.in_default = self.Port(self.INPUTS_TYPE.empty()) - self.in_is_connected = self.ArgParameter(in_is_connected) - self.generator_param(self.in_is_connected) - - @override - def generate(self) -> None: - super().generate() - if self.get(self.in_is_connected): - self.connect(self.out, self.in_connected) - self.in_default.init_from(self.INPUTS_TYPE()) # create ideal port - else: - self.connect(self.out, self.in_default) - # no ideal port needed, this should be invalid anyways - - def out_with_default(self: SelfType, out: Port[LinkType], in_connected: Port[LinkType], - in_default: Port[LinkType]) -> SelfType: - # note this runs in parent scope, so in_is_connected is valid - parent = builder.get_enclosing_block() - assert parent is not None - parent.connect(self.out, out) - parent.connect(self.in_connected, in_connected) - parent.connect(self.in_default, in_default) - parent.assign(self.in_is_connected, in_connected.is_connected()) - return self + """The template actually lives here""" + + INPUTS_TYPE: Type[Port] + OUTPUT_TYPE: Type[Port] + + def __init__(self, in_is_connected: BoolLike = BoolExpr()) -> None: + """in_is_connected needs to be connected from above, since from the perspective + of this block, the input is always (locally) connected""" + super().__init__() + self.out = self.Port(self.OUTPUT_TYPE.empty()) + self.in_connected = self.Port(self.INPUTS_TYPE.empty(), optional=True) + self.in_default = self.Port(self.INPUTS_TYPE.empty()) + self.in_is_connected = self.ArgParameter(in_is_connected) + self.generator_param(self.in_is_connected) + + @override + def generate(self) -> None: + super().generate() + if self.get(self.in_is_connected): + self.connect(self.out, self.in_connected) + self.in_default.init_from(self.INPUTS_TYPE()) # create ideal port + else: + self.connect(self.out, self.in_default) + # no ideal port needed, this should be invalid anyways + + def out_with_default( + self: SelfType, out: Port[LinkType], in_connected: Port[LinkType], in_default: Port[LinkType] + ) -> SelfType: + # note this runs in parent scope, so in_is_connected is valid + parent = builder.get_enclosing_block() + assert parent is not None + parent.connect(self.out, out) + parent.connect(self.in_connected, in_connected) + parent.connect(self.in_default, in_default) + parent.assign(self.in_is_connected, in_connected.is_connected()) + return self class VoltageSourceConnected(BaseConnectedGenerator[VoltageSource, VoltageSink, VoltageLink]): - OUTPUT_TYPE = VoltageSource - INPUTS_TYPE = VoltageSink + OUTPUT_TYPE = VoltageSource + INPUTS_TYPE = VoltageSink class DigitalSourceConnected(BaseConnectedGenerator[DigitalSource, DigitalSink, DigitalLink]): - OUTPUT_TYPE = DigitalSource - INPUTS_TYPE = DigitalSink + OUTPUT_TYPE = DigitalSource + INPUTS_TYPE = DigitalSink diff --git a/edg/electronics_model/CrystalPort.py b/edg/electronics_model/CrystalPort.py index a7d599b86..e289539d9 100644 --- a/edg/electronics_model/CrystalPort.py +++ b/edg/electronics_model/CrystalPort.py @@ -5,42 +5,41 @@ class CrystalLink(Link): - def __init__(self) -> None: - super().__init__() - self.driver = self.Port(CrystalDriver()) - self.crystal = self.Port(CrystalPort()) + def __init__(self) -> None: + super().__init__() + self.driver = self.Port(CrystalDriver()) + self.crystal = self.Port(CrystalPort()) - self.drive_voltage = self.Parameter(RangeExpr(self.driver.voltage_out)) - self.frequency = self.Parameter(RangeExpr(self.crystal.frequency)) + self.drive_voltage = self.Parameter(RangeExpr(self.driver.voltage_out)) + self.frequency = self.Parameter(RangeExpr(self.crystal.frequency)) - @override - def contents(self) -> None: - super().contents() - self.require(self.driver.frequency_limits.contains(self.frequency)) + @override + def contents(self) -> None: + super().contents() + self.require(self.driver.frequency_limits.contains(self.frequency)) - self.xi = self.connect(self.driver.xtal_in, self.crystal.xtal_in) - self.xo = self.connect(self.driver.xtal_out, self.crystal.xtal_out) + self.xi = self.connect(self.driver.xtal_in, self.crystal.xtal_in) + self.xo = self.connect(self.driver.xtal_out, self.crystal.xtal_out) class CrystalPort(Bundle[CrystalLink]): - link_type = CrystalLink + link_type = CrystalLink - def __init__(self, frequency: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() - self.xtal_in = self.Port(Passive()) - self.xtal_out = self.Port(Passive()) + def __init__(self, frequency: RangeLike = RangeExpr.ZERO) -> None: + super().__init__() + self.xtal_in = self.Port(Passive()) + self.xtal_out = self.Port(Passive()) - self.frequency = self.Parameter(RangeExpr(frequency)) + self.frequency = self.Parameter(RangeExpr(frequency)) class CrystalDriver(Bundle[CrystalLink]): - link_type = CrystalLink + link_type = CrystalLink - def __init__(self, frequency_limits: RangeLike = RangeExpr.ALL, - voltage_out: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() - self.voltage_out = self.Parameter(RangeExpr(voltage_out)) - self.xtal_in = self.Port(Passive()) - self.xtal_out = self.Port(Passive()) + def __init__(self, frequency_limits: RangeLike = RangeExpr.ALL, voltage_out: RangeLike = RangeExpr.ZERO) -> None: + super().__init__() + self.voltage_out = self.Parameter(RangeExpr(voltage_out)) + self.xtal_in = self.Port(Passive()) + self.xtal_out = self.Port(Passive()) - self.frequency_limits = self.Parameter(RangeExpr(frequency_limits)) + self.frequency_limits = self.Parameter(RangeExpr(frequency_limits)) diff --git a/edg/electronics_model/DebugPorts.py b/edg/electronics_model/DebugPorts.py index 2457a559e..3e70b9a56 100644 --- a/edg/electronics_model/DebugPorts.py +++ b/edg/electronics_model/DebugPorts.py @@ -7,51 +7,53 @@ class SwdLink(Link): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.host = self.Port(SwdHostPort.empty()) - self.device = self.Port(SwdTargetPort.empty()) - self.pull = self.Port(Vector(SwdPullPort.empty()), optional=True) + self.host = self.Port(SwdHostPort.empty()) + self.device = self.Port(SwdTargetPort.empty()) + self.pull = self.Port(Vector(SwdPullPort.empty()), optional=True) - @override - def contents(self) -> None: - super().contents() - - self.swdio = self.connect(self.host.swdio, self.device.swdio, self.pull.map_extract(lambda port: port.swdio), - flatten=True) - self.swclk = self.connect(self.host.swclk, self.device.swclk, self.pull.map_extract(lambda port: port.swclk), - flatten=True) + @override + def contents(self) -> None: + super().contents() + + self.swdio = self.connect( + self.host.swdio, self.device.swdio, self.pull.map_extract(lambda port: port.swdio), flatten=True + ) + self.swclk = self.connect( + self.host.swclk, self.device.swclk, self.pull.map_extract(lambda port: port.swclk), flatten=True + ) class SwdHostPort(Bundle[SwdLink]): - link_type = SwdLink + link_type = SwdLink - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.swdio = self.Port(model) - self.swclk = self.Port(DigitalSource.from_bidir(model)) + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.swdio = self.Port(model) + self.swclk = self.Port(DigitalSource.from_bidir(model)) class SwdTargetPort(Bundle[SwdLink]): - link_type = SwdLink + link_type = SwdLink - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.swdio = self.Port(model) - self.swclk = self.Port(DigitalSink.from_bidir(model)) + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.swdio = self.Port(model) + self.swclk = self.Port(DigitalSink.from_bidir(model)) class SwdPullPort(Bundle[SwdLink]): - link_type = SwdLink - - def __init__(self, model: Optional[DigitalSource] = None) -> None: - super().__init__() - if model is None: - model = DigitalSource() # ideal by default - self.swdio = self.Port(model) - self.swclk = self.Port(model) + link_type = SwdLink + + def __init__(self, model: Optional[DigitalSource] = None) -> None: + super().__init__() + if model is None: + model = DigitalSource() # ideal by default + self.swdio = self.Port(model) + self.swclk = self.Port(model) diff --git a/edg/electronics_model/DigitalPorts.py b/edg/electronics_model/DigitalPorts.py index 94b05796b..d28e21c10 100644 --- a/edg/electronics_model/DigitalPorts.py +++ b/edg/electronics_model/DigitalPorts.py @@ -13,501 +13,611 @@ class DigitalLink(CircuitLink): - """A link for digital IOs. Because of the wide variations on digital IOs, this is kind of a beast. - - Overall, this means a port that deals with signals that can be driven to two levels, high or low. - Directionality is modeled as signal dataflow. - The types of ports are: - - Source: can drive high and/or low (including push-pull, pull-up, and open-drain), but can't read. - Push-pull sources assumed not able to tri-state and cannot share the line with other push-pull drivers. - - Sink: cannot drive, but can read. - - Bidir: can drive both high and low, and can read. Can tri-state, and assumed ports are configured to not conflict. - - Sources can be modeled as high and/or low-side drivers. If not push-pull, an opposite-polarity pull is required. - Pulls do not need a complementary driver and can be used to provide a default state. - Sources and bidir are modeled as being pull-capable. - """ - # can't subclass VoltageLink because the constraint behavior is slightly different with presence of Bidir - - def __init__(self) -> None: - super().__init__() - - self.sources = self.Port(Vector(DigitalSource()), optional=True) - self.sinks = self.Port(Vector(DigitalSink()), optional=True) - self.bidirs = self.Port(Vector(DigitalBidir()), optional=True) - - self.voltage = self.Parameter(RangeExpr()) - self.voltage_limits = self.Parameter(RangeExpr()) - - self.current_drawn = self.Parameter(RangeExpr()) - self.current_limits = self.Parameter(RangeExpr()) - - self.output_thresholds = self.Parameter(RangeExpr()) - self.input_thresholds = self.Parameter(RangeExpr()) - - self.pullup_capable = self.Parameter(BoolExpr()) - self.pulldown_capable = self.Parameter(BoolExpr()) - - # these are only used for internal checks - self._has_low_signal_driver = self.Parameter(BoolExpr()) - self._has_high_signal_driver = self.Parameter(BoolExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "voltage: ", DescriptionString.FormatUnits(self.voltage, "V"), - " of limits: ", DescriptionString.FormatUnits(self.voltage_limits, "V"), - "\ncurrent: ", DescriptionString.FormatUnits(self.current_drawn, "A"), - " of limits: ", DescriptionString.FormatUnits(self.current_limits, "A"), - "\noutput thresholds: ", DescriptionString.FormatUnits(self.output_thresholds, "V"), - ", input thresholds: ", DescriptionString.FormatUnits(self.input_thresholds, "V")) - - # TODO clean this up, massively, like, this needs new constructs to simplify this pattern - voltage_hull = self.bidirs.hull(lambda x: x.voltage_out) - voltage_hull = self.sources.any_connected().then_else( - voltage_hull.hull(self.sources.hull(lambda x: x.voltage_out)), - voltage_hull - ) - self.assign(self.voltage, voltage_hull) - - self.assign(self.voltage_limits, - self.sinks.intersection(lambda x: x.voltage_limits).intersect(self.bidirs.intersection(lambda x: x.voltage_limits)) - ) - self.require(self.voltage_limits.contains(self.voltage), "overvoltage") - - self.assign(self.current_drawn, - self.sinks.sum(lambda x: x.current_draw) + self.bidirs.sum(lambda x: x.current_draw) - ) - self.assign(self.current_limits, - self.sources.intersection(lambda x: x.current_limits) - .intersect(self.bidirs.intersection(lambda x: x.current_limits))) - self.require(self.current_limits.contains(self.current_drawn), "overcurrent") - - self.assign(self.output_thresholds, - self.sources.intersection(lambda x: x.output_thresholds) - .intersect(self.bidirs.intersection(lambda x: x.output_thresholds),)) - self.assign(self.input_thresholds, - self.sinks.hull(lambda x: x.input_thresholds).hull(self.bidirs.hull(lambda x: x.input_thresholds)), - ) - self.require(self.output_thresholds.contains(self.input_thresholds), "incompatible digital thresholds") - - self.require(self.sources.any_connected() | (self.bidirs.length() > 0), - "requires connected source or bidir") - - # ensure both digital levels can be driven (but pull-up or -down only connections are allowed) - self.assign(self.pullup_capable, - self.sources.any(lambda x: x.pullup_capable) | - self.sinks.any(lambda x: x.pullup_capable) | - self.bidirs.any(lambda x: x.pullup_capable)) - self.assign(self.pulldown_capable, - self.sources.any(lambda x: x.pulldown_capable) | - self.sinks.any(lambda x: x.pulldown_capable) | - self.bidirs.any(lambda x: x.pulldown_capable)) - self.assign(self._has_low_signal_driver, # assumed bidirs are true directional drivers - self.bidirs.any_connected() | self.sources.any(lambda x: x.low_driver)) - self.assign(self._has_high_signal_driver, - self.bidirs.any_connected() | self.sources.any(lambda x: x.high_driver)) - - is_bridged_internal = (self.sources.any(lambda x: x._bridged_internal) | - self.sinks.any(lambda x: x._bridged_internal) | - self.bidirs.any(lambda x: x._bridged_internal)) - self.require(is_bridged_internal | - self._has_high_signal_driver.implies(self._has_low_signal_driver | self.pulldown_capable), "requires low driver or pulldown") - self.require(is_bridged_internal | - self._has_low_signal_driver.implies(self._has_high_signal_driver | self.pullup_capable), "requires high driver or pullup") - - # when multiple sources, ensure they all drive only one signal direction (eg, open drain) - self.require((self.sources.count(lambda x: x.high_driver) > 1).implies(~self.sources.any(lambda x: x.low_driver)) & - (self.sources.count(lambda x: x.low_driver) > 1).implies(~self.sources.any(lambda x: x.high_driver)), - "conflicting source drivers") + """A link for digital IOs. Because of the wide variations on digital IOs, this is kind of a beast. + + Overall, this means a port that deals with signals that can be driven to two levels, high or low. + Directionality is modeled as signal dataflow. + The types of ports are: + - Source: can drive high and/or low (including push-pull, pull-up, and open-drain), but can't read. + Push-pull sources assumed not able to tri-state and cannot share the line with other push-pull drivers. + - Sink: cannot drive, but can read. + - Bidir: can drive both high and low, and can read. Can tri-state, and assumed ports are configured to not conflict. + + Sources can be modeled as high and/or low-side drivers. If not push-pull, an opposite-polarity pull is required. + Pulls do not need a complementary driver and can be used to provide a default state. + Sources and bidir are modeled as being pull-capable. + """ + + # can't subclass VoltageLink because the constraint behavior is slightly different with presence of Bidir + + def __init__(self) -> None: + super().__init__() + + self.sources = self.Port(Vector(DigitalSource()), optional=True) + self.sinks = self.Port(Vector(DigitalSink()), optional=True) + self.bidirs = self.Port(Vector(DigitalBidir()), optional=True) + + self.voltage = self.Parameter(RangeExpr()) + self.voltage_limits = self.Parameter(RangeExpr()) + + self.current_drawn = self.Parameter(RangeExpr()) + self.current_limits = self.Parameter(RangeExpr()) + + self.output_thresholds = self.Parameter(RangeExpr()) + self.input_thresholds = self.Parameter(RangeExpr()) + + self.pullup_capable = self.Parameter(BoolExpr()) + self.pulldown_capable = self.Parameter(BoolExpr()) + + # these are only used for internal checks + self._has_low_signal_driver = self.Parameter(BoolExpr()) + self._has_high_signal_driver = self.Parameter(BoolExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "voltage: ", + DescriptionString.FormatUnits(self.voltage, "V"), + " of limits: ", + DescriptionString.FormatUnits(self.voltage_limits, "V"), + "\ncurrent: ", + DescriptionString.FormatUnits(self.current_drawn, "A"), + " of limits: ", + DescriptionString.FormatUnits(self.current_limits, "A"), + "\noutput thresholds: ", + DescriptionString.FormatUnits(self.output_thresholds, "V"), + ", input thresholds: ", + DescriptionString.FormatUnits(self.input_thresholds, "V"), + ) + + # TODO clean this up, massively, like, this needs new constructs to simplify this pattern + voltage_hull = self.bidirs.hull(lambda x: x.voltage_out) + voltage_hull = self.sources.any_connected().then_else( + voltage_hull.hull(self.sources.hull(lambda x: x.voltage_out)), voltage_hull + ) + self.assign(self.voltage, voltage_hull) + + self.assign( + self.voltage_limits, + self.sinks.intersection(lambda x: x.voltage_limits).intersect( + self.bidirs.intersection(lambda x: x.voltage_limits) + ), + ) + self.require(self.voltage_limits.contains(self.voltage), "overvoltage") + + self.assign( + self.current_drawn, self.sinks.sum(lambda x: x.current_draw) + self.bidirs.sum(lambda x: x.current_draw) + ) + self.assign( + self.current_limits, + self.sources.intersection(lambda x: x.current_limits).intersect( + self.bidirs.intersection(lambda x: x.current_limits) + ), + ) + self.require(self.current_limits.contains(self.current_drawn), "overcurrent") + + self.assign( + self.output_thresholds, + self.sources.intersection(lambda x: x.output_thresholds).intersect( + self.bidirs.intersection(lambda x: x.output_thresholds), + ), + ) + self.assign( + self.input_thresholds, + self.sinks.hull(lambda x: x.input_thresholds).hull(self.bidirs.hull(lambda x: x.input_thresholds)), + ) + self.require(self.output_thresholds.contains(self.input_thresholds), "incompatible digital thresholds") + + self.require(self.sources.any_connected() | (self.bidirs.length() > 0), "requires connected source or bidir") + + # ensure both digital levels can be driven (but pull-up or -down only connections are allowed) + self.assign( + self.pullup_capable, + self.sources.any(lambda x: x.pullup_capable) + | self.sinks.any(lambda x: x.pullup_capable) + | self.bidirs.any(lambda x: x.pullup_capable), + ) + self.assign( + self.pulldown_capable, + self.sources.any(lambda x: x.pulldown_capable) + | self.sinks.any(lambda x: x.pulldown_capable) + | self.bidirs.any(lambda x: x.pulldown_capable), + ) + self.assign( + self._has_low_signal_driver, # assumed bidirs are true directional drivers + self.bidirs.any_connected() | self.sources.any(lambda x: x.low_driver), + ) + self.assign( + self._has_high_signal_driver, self.bidirs.any_connected() | self.sources.any(lambda x: x.high_driver) + ) + + is_bridged_internal = ( + self.sources.any(lambda x: x._bridged_internal) + | self.sinks.any(lambda x: x._bridged_internal) + | self.bidirs.any(lambda x: x._bridged_internal) + ) + self.require( + is_bridged_internal + | self._has_high_signal_driver.implies(self._has_low_signal_driver | self.pulldown_capable), + "requires low driver or pulldown", + ) + self.require( + is_bridged_internal + | self._has_low_signal_driver.implies(self._has_high_signal_driver | self.pullup_capable), + "requires high driver or pullup", + ) + + # when multiple sources, ensure they all drive only one signal direction (eg, open drain) + self.require( + (self.sources.count(lambda x: x.high_driver) > 1).implies(~self.sources.any(lambda x: x.low_driver)) + & (self.sources.count(lambda x: x.low_driver) > 1).implies(~self.sources.any(lambda x: x.high_driver)), + "conflicting source drivers", + ) class DigitalBase(CircuitPort[DigitalLink]): - link_type = DigitalLink + link_type = DigitalLink class DigitalSinkBridge(CircuitPortBridge): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.outer_port = self.Port(DigitalSink(voltage_limits=RangeExpr(), - current_draw=RangeExpr(), - input_thresholds=RangeExpr())) + self.outer_port = self.Port( + DigitalSink(voltage_limits=RangeExpr(), current_draw=RangeExpr(), input_thresholds=RangeExpr()) + ) - self.inner_link = self.Port(DigitalSource(current_limits=RangeExpr.ALL, - voltage_out=RangeExpr(), - output_thresholds=RangeExpr(), - pullup_capable=False, pulldown_capable=False, # don't create a loop - _bridged_internal=True)) + self.inner_link = self.Port( + DigitalSource( + current_limits=RangeExpr.ALL, + voltage_out=RangeExpr(), + output_thresholds=RangeExpr(), + pullup_capable=False, + pulldown_capable=False, # don't create a loop + _bridged_internal=True, + ) + ) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.assign(self.outer_port.voltage_limits, self.inner_link.link().voltage_limits) - self.assign(self.outer_port.current_draw, self.inner_link.link().current_drawn) - self.assign(self.outer_port.input_thresholds, self.inner_link.link().input_thresholds) + self.assign(self.outer_port.voltage_limits, self.inner_link.link().voltage_limits) + self.assign(self.outer_port.current_draw, self.inner_link.link().current_drawn) + self.assign(self.outer_port.input_thresholds, self.inner_link.link().input_thresholds) - self.assign(self.inner_link.voltage_out, self.outer_port.link().voltage) - self.assign(self.inner_link.output_thresholds, self.outer_port.link().output_thresholds) + self.assign(self.inner_link.voltage_out, self.outer_port.link().voltage) + self.assign(self.inner_link.output_thresholds, self.outer_port.link().output_thresholds) class DigitalSink(DigitalBase): - bridge_type = DigitalSinkBridge - - @staticmethod - def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink], *, - voltage_limit_abs: Optional[RangeLike] = None, - voltage_limit_tolerance: Optional[RangeLike] = None, - current_draw: RangeLike = RangeExpr.ZERO, - input_threshold_factor: Optional[RangeLike] = None, - input_threshold_abs: Optional[RangeLike] = None, - pullup_capable: BoolLike = False, - pulldown_capable: BoolLike = False) -> DigitalSink: - supply_range = VoltageLink._supply_voltage_range(neg, pos) - if voltage_limit_abs is not None: - assert voltage_limit_tolerance is None - voltage_limit: RangeLike = voltage_limit_abs - elif voltage_limit_tolerance is not None: - voltage_limit = supply_range + RangeExpr._to_expr_type(voltage_limit_tolerance) - else: # generic default - voltage_limit = supply_range + RangeExpr._to_expr_type((-0.3, 0.3)) - - input_threshold: RangeLike - if input_threshold_factor is not None: - assert input_threshold_abs is None, "can only specify one input threshold type" - input_threshold_factor = RangeExpr._to_expr_type(input_threshold_factor) # TODO avoid internal functions? - input_threshold = pos.link().voltage * input_threshold_factor - elif input_threshold_abs is not None: - assert input_threshold_factor is None, "can only specify one input threshold type" - input_threshold = RangeExpr._to_expr_type(input_threshold_abs) # TODO avoid internal functions? - else: - input_threshold = RangeExpr.EMPTY # ideal - - return DigitalSink( # TODO get rid of to_expr_type w/ dedicated Range conversion - voltage_limits=voltage_limit, - current_draw=current_draw, - input_thresholds=input_threshold, - pullup_capable=pullup_capable, - pulldown_capable=pulldown_capable - ) - - @staticmethod - def from_bidir(model: DigitalBidir) -> DigitalSink: - model_is_empty = not model._get_initializers([]) - if not model_is_empty: - return DigitalSink(model.voltage_limits, model.current_draw, input_thresholds=model.input_thresholds, - pulldown_capable=model.pulldown_capable, pullup_capable=model.pullup_capable) - else: - return DigitalSink.empty() - - def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO, *, - input_thresholds: RangeLike = RangeExpr.EMPTY, - pullup_capable: BoolLike = False, - pulldown_capable: BoolLike = False, - _bridged_internal: BoolLike = False) -> None: - super().__init__() - self.voltage_limits: RangeExpr = self.Parameter(RangeExpr(voltage_limits)) - self.current_draw: RangeExpr = self.Parameter(RangeExpr(current_draw)) - self.input_thresholds: RangeExpr = self.Parameter(RangeExpr(input_thresholds)) - - self.pullup_capable: BoolExpr = self.Parameter(BoolExpr(pullup_capable)) - self.pulldown_capable: BoolExpr = self.Parameter(BoolExpr(pulldown_capable)) - self._bridged_internal: BoolExpr = self.Parameter(BoolExpr(_bridged_internal)) + bridge_type = DigitalSinkBridge + + @staticmethod + def from_supply( + neg: Port[GroundLink], + pos: Port[VoltageLink], + *, + voltage_limit_abs: Optional[RangeLike] = None, + voltage_limit_tolerance: Optional[RangeLike] = None, + current_draw: RangeLike = RangeExpr.ZERO, + input_threshold_factor: Optional[RangeLike] = None, + input_threshold_abs: Optional[RangeLike] = None, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + ) -> DigitalSink: + supply_range = VoltageLink._supply_voltage_range(neg, pos) + if voltage_limit_abs is not None: + assert voltage_limit_tolerance is None + voltage_limit: RangeLike = voltage_limit_abs + elif voltage_limit_tolerance is not None: + voltage_limit = supply_range + RangeExpr._to_expr_type(voltage_limit_tolerance) + else: # generic default + voltage_limit = supply_range + RangeExpr._to_expr_type((-0.3, 0.3)) + + input_threshold: RangeLike + if input_threshold_factor is not None: + assert input_threshold_abs is None, "can only specify one input threshold type" + input_threshold_factor = RangeExpr._to_expr_type(input_threshold_factor) # TODO avoid internal functions? + input_threshold = pos.link().voltage * input_threshold_factor + elif input_threshold_abs is not None: + assert input_threshold_factor is None, "can only specify one input threshold type" + input_threshold = RangeExpr._to_expr_type(input_threshold_abs) # TODO avoid internal functions? + else: + input_threshold = RangeExpr.EMPTY # ideal + + return DigitalSink( # TODO get rid of to_expr_type w/ dedicated Range conversion + voltage_limits=voltage_limit, + current_draw=current_draw, + input_thresholds=input_threshold, + pullup_capable=pullup_capable, + pulldown_capable=pulldown_capable, + ) + + @staticmethod + def from_bidir(model: DigitalBidir) -> DigitalSink: + model_is_empty = not model._get_initializers([]) + if not model_is_empty: + return DigitalSink( + model.voltage_limits, + model.current_draw, + input_thresholds=model.input_thresholds, + pulldown_capable=model.pulldown_capable, + pullup_capable=model.pullup_capable, + ) + else: + return DigitalSink.empty() + + def __init__( + self, + voltage_limits: RangeLike = RangeExpr.ALL, + current_draw: RangeLike = RangeExpr.ZERO, + *, + input_thresholds: RangeLike = RangeExpr.EMPTY, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + _bridged_internal: BoolLike = False, + ) -> None: + super().__init__() + self.voltage_limits: RangeExpr = self.Parameter(RangeExpr(voltage_limits)) + self.current_draw: RangeExpr = self.Parameter(RangeExpr(current_draw)) + self.input_thresholds: RangeExpr = self.Parameter(RangeExpr(input_thresholds)) + + self.pullup_capable: BoolExpr = self.Parameter(BoolExpr(pullup_capable)) + self.pulldown_capable: BoolExpr = self.Parameter(BoolExpr(pulldown_capable)) + self._bridged_internal: BoolExpr = self.Parameter(BoolExpr(_bridged_internal)) class DigitalSourceBridge(CircuitPortBridge): - def __init__(self) -> None: - super().__init__() - - self.outer_port = self.Port(DigitalSource(voltage_out=RangeExpr(), - current_limits=RangeExpr(), - output_thresholds=RangeExpr())) - - # Here we ignore the voltage_limits of the inner port, instead relying on the main link to handle it - # The outer port's current_limits is untouched and should be defined in tte port def. - # TODO: it's a slightly optimization to handle them here. Should it be done? - # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? - self.inner_link = self.Port(DigitalSink(voltage_limits=RangeExpr.ALL, - current_draw=RangeExpr(), - input_thresholds=RangeExpr.EMPTY, - pullup_capable=False, pulldown_capable=False, # don't create a loop - _bridged_internal=True)) - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.outer_port.voltage_out, self.inner_link.link().voltage) - self.assign(self.outer_port.current_limits, self.inner_link.link().current_limits) # TODO subtract internal current drawn - self.assign(self.inner_link.current_draw, self.outer_port.link().current_drawn) - - self.assign(self.outer_port.output_thresholds, self.inner_link.link().output_thresholds) + def __init__(self) -> None: + super().__init__() + + self.outer_port = self.Port( + DigitalSource(voltage_out=RangeExpr(), current_limits=RangeExpr(), output_thresholds=RangeExpr()) + ) + + # Here we ignore the voltage_limits of the inner port, instead relying on the main link to handle it + # The outer port's current_limits is untouched and should be defined in tte port def. + # TODO: it's a slightly optimization to handle them here. Should it be done? + # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? + self.inner_link = self.Port( + DigitalSink( + voltage_limits=RangeExpr.ALL, + current_draw=RangeExpr(), + input_thresholds=RangeExpr.EMPTY, + pullup_capable=False, + pulldown_capable=False, # don't create a loop + _bridged_internal=True, + ) + ) + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.outer_port.voltage_out, self.inner_link.link().voltage) + self.assign( + self.outer_port.current_limits, self.inner_link.link().current_limits + ) # TODO subtract internal current drawn + self.assign(self.inner_link.current_draw, self.outer_port.link().current_drawn) + + self.assign(self.outer_port.output_thresholds, self.inner_link.link().output_thresholds) class DigitalSourceAdapterVoltageSource(CircuitPortAdapter[VoltageSource]): - def __init__(self) -> None: - super().__init__() - self.src = self.Port(DigitalSink( # otherwise ideal - current_draw=RangeExpr() - )) - self.dst = self.Port(VoltageSource( - voltage_out=(self.src.link().output_thresholds.upper(), self.src.link().voltage.upper()) - )) - self.assign(self.src.current_draw, self.dst.link().current_drawn) + def __init__(self) -> None: + super().__init__() + self.src = self.Port(DigitalSink(current_draw=RangeExpr())) # otherwise ideal + self.dst = self.Port( + VoltageSource(voltage_out=(self.src.link().output_thresholds.upper(), self.src.link().voltage.upper())) + ) + self.assign(self.src.current_draw, self.dst.link().current_drawn) class DigitalSource(DigitalBase): - bridge_type = DigitalSourceBridge - - @staticmethod - def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink], - current_limits: RangeLike = RangeExpr.ALL, *, - output_threshold_offset: Optional[Tuple[FloatLike, FloatLike]] = None) -> DigitalSource: - supply_range = VoltageLink._supply_voltage_range(neg, pos) - if output_threshold_offset is not None: - output_offset_low = FloatExpr._to_expr_type(output_threshold_offset[0]) - output_offset_high = FloatExpr._to_expr_type(output_threshold_offset[1]) - output_threshold = (GroundLink._voltage_range(neg).lower() + output_offset_low, - VoltageLink._voltage_range(pos).lower() + output_offset_high) - else: - output_threshold = (GroundLink._voltage_range(neg).upper(), VoltageLink._voltage_range(pos).lower()) - - return DigitalSource( - voltage_out=supply_range, - current_limits=current_limits, - output_thresholds=output_threshold - ) - - @staticmethod - def from_bidir(model: DigitalBidir) -> DigitalSource: - model_is_empty = not model._get_initializers([]) - if not model_is_empty: # DigitalSource has additional high_driver and low_driver fields - return DigitalSource(model.voltage_out, model.current_limits, output_thresholds=model.output_thresholds, - pullup_capable=model.pullup_capable, pulldown_capable=model.pulldown_capable) - else: - return DigitalSource.empty() - - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL, *, - output_thresholds: RangeLike = RangeExpr.ALL, - high_driver: BoolLike = True, - low_driver: BoolLike = True, - pullup_capable: BoolLike = False, - pulldown_capable: BoolLike = False, - _bridged_internal: BoolLike = False) -> None: - super().__init__() - self.voltage_out: RangeExpr = self.Parameter(RangeExpr(voltage_out)) - self.current_limits: RangeExpr = self.Parameter(RangeExpr(current_limits)) - self.output_thresholds: RangeExpr = self.Parameter(RangeExpr(output_thresholds)) - - self.high_driver: BoolExpr = self.Parameter(BoolExpr(high_driver)) - self.low_driver: BoolExpr = self.Parameter(BoolExpr(low_driver)) - self.pullup_capable: BoolExpr = self.Parameter(BoolExpr(pullup_capable)) - self.pulldown_capable: BoolExpr = self.Parameter(BoolExpr(pulldown_capable)) - - self._bridged_internal: BoolExpr = self.Parameter(BoolExpr(_bridged_internal)) - - @staticmethod - def low_from_supply(neg: Port[GroundLink], *, current_limits: RangeLike = RangeExpr.ALL) -> DigitalSource: - """Sink-only digital source, eg open-drain output""" - return DigitalSource( - voltage_out=neg.link().voltage, - current_limits=current_limits, - output_thresholds=(neg.link().voltage.upper(), float('inf')), - high_driver=False, low_driver=True, - pullup_capable=False, pulldown_capable=False - ) - - @staticmethod - def high_from_supply(pos: Port[VoltageLink], *, current_limits: RangeLike = RangeExpr.ALL) -> DigitalSource: - """Source-only digital source""" - return DigitalSource( - voltage_out=pos.link().voltage, - current_limits=current_limits, - output_thresholds=(-float('inf'), pos.link().voltage.lower()), - high_driver=True, low_driver=False, - pullup_capable=False, pulldown_capable=False - ) - - @staticmethod - def pulldown_from_supply(neg: Port[GroundLink]) -> DigitalSource: - return DigitalSource( - voltage_out=neg.link().voltage, - output_thresholds=(neg.link().voltage.upper(), float('inf')), - high_driver=False, low_driver=False, - pullup_capable=False, pulldown_capable=True - ) - - @staticmethod - def pullup_from_supply(pos: Port[VoltageLink]) -> DigitalSource: - return DigitalSource( - voltage_out=pos.link().voltage, - output_thresholds=(-float('inf'), pos.link().voltage.lower()), - high_driver=False, low_driver=False, - pullup_capable=True, pulldown_capable=False - ) - - def as_voltage_source(self) -> VoltageSource: - return self._convert(DigitalSourceAdapterVoltageSource()) + bridge_type = DigitalSourceBridge + + @staticmethod + def from_supply( + neg: Port[GroundLink], + pos: Port[VoltageLink], + current_limits: RangeLike = RangeExpr.ALL, + *, + output_threshold_offset: Optional[Tuple[FloatLike, FloatLike]] = None, + ) -> DigitalSource: + supply_range = VoltageLink._supply_voltage_range(neg, pos) + if output_threshold_offset is not None: + output_offset_low = FloatExpr._to_expr_type(output_threshold_offset[0]) + output_offset_high = FloatExpr._to_expr_type(output_threshold_offset[1]) + output_threshold = ( + GroundLink._voltage_range(neg).lower() + output_offset_low, + VoltageLink._voltage_range(pos).lower() + output_offset_high, + ) + else: + output_threshold = (GroundLink._voltage_range(neg).upper(), VoltageLink._voltage_range(pos).lower()) + + return DigitalSource( + voltage_out=supply_range, current_limits=current_limits, output_thresholds=output_threshold + ) + + @staticmethod + def from_bidir(model: DigitalBidir) -> DigitalSource: + model_is_empty = not model._get_initializers([]) + if not model_is_empty: # DigitalSource has additional high_driver and low_driver fields + return DigitalSource( + model.voltage_out, + model.current_limits, + output_thresholds=model.output_thresholds, + pullup_capable=model.pullup_capable, + pulldown_capable=model.pulldown_capable, + ) + else: + return DigitalSource.empty() + + def __init__( + self, + voltage_out: RangeLike = RangeExpr.ZERO, + current_limits: RangeLike = RangeExpr.ALL, + *, + output_thresholds: RangeLike = RangeExpr.ALL, + high_driver: BoolLike = True, + low_driver: BoolLike = True, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + _bridged_internal: BoolLike = False, + ) -> None: + super().__init__() + self.voltage_out: RangeExpr = self.Parameter(RangeExpr(voltage_out)) + self.current_limits: RangeExpr = self.Parameter(RangeExpr(current_limits)) + self.output_thresholds: RangeExpr = self.Parameter(RangeExpr(output_thresholds)) + + self.high_driver: BoolExpr = self.Parameter(BoolExpr(high_driver)) + self.low_driver: BoolExpr = self.Parameter(BoolExpr(low_driver)) + self.pullup_capable: BoolExpr = self.Parameter(BoolExpr(pullup_capable)) + self.pulldown_capable: BoolExpr = self.Parameter(BoolExpr(pulldown_capable)) + + self._bridged_internal: BoolExpr = self.Parameter(BoolExpr(_bridged_internal)) + + @staticmethod + def low_from_supply(neg: Port[GroundLink], *, current_limits: RangeLike = RangeExpr.ALL) -> DigitalSource: + """Sink-only digital source, eg open-drain output""" + return DigitalSource( + voltage_out=neg.link().voltage, + current_limits=current_limits, + output_thresholds=(neg.link().voltage.upper(), float("inf")), + high_driver=False, + low_driver=True, + pullup_capable=False, + pulldown_capable=False, + ) + + @staticmethod + def high_from_supply(pos: Port[VoltageLink], *, current_limits: RangeLike = RangeExpr.ALL) -> DigitalSource: + """Source-only digital source""" + return DigitalSource( + voltage_out=pos.link().voltage, + current_limits=current_limits, + output_thresholds=(-float("inf"), pos.link().voltage.lower()), + high_driver=True, + low_driver=False, + pullup_capable=False, + pulldown_capable=False, + ) + + @staticmethod + def pulldown_from_supply(neg: Port[GroundLink]) -> DigitalSource: + return DigitalSource( + voltage_out=neg.link().voltage, + output_thresholds=(neg.link().voltage.upper(), float("inf")), + high_driver=False, + low_driver=False, + pullup_capable=False, + pulldown_capable=True, + ) + + @staticmethod + def pullup_from_supply(pos: Port[VoltageLink]) -> DigitalSource: + return DigitalSource( + voltage_out=pos.link().voltage, + output_thresholds=(-float("inf"), pos.link().voltage.lower()), + high_driver=False, + low_driver=False, + pullup_capable=True, + pulldown_capable=False, + ) + + def as_voltage_source(self) -> VoltageSource: + return self._convert(DigitalSourceAdapterVoltageSource()) class DigitalBidirBridge(CircuitPortBridge): - def __init__(self) -> None: - super().__init__() - - self.outer_port = self.Port(DigitalBidir(voltage_out=RangeExpr(), current_draw=RangeExpr(), - voltage_limits=RangeExpr(), current_limits=RangeExpr(), - output_thresholds=RangeExpr(), input_thresholds=RangeExpr(), - pulldown_capable=BoolExpr(), pullup_capable=BoolExpr(), - )) - # TODO can we actually define something here? as a pseudoport, this doesn't have limits - self.inner_link = self.Port(DigitalBidir(voltage_limits=RangeExpr.ALL, current_limits=RangeExpr.ALL, - pullup_capable=False, pulldown_capable=False, # don't create a loop - _bridged_internal=True - )) - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.outer_port.voltage_out, self.inner_link.link().voltage) - self.assign(self.outer_port.current_draw, self.inner_link.link().current_drawn) - self.assign(self.outer_port.voltage_limits, self.inner_link.link().voltage_limits) - self.assign(self.outer_port.current_limits, self.inner_link.link().current_limits) # TODO compensate for internal current draw - - self.assign(self.outer_port.output_thresholds, self.inner_link.link().output_thresholds) - self.assign(self.outer_port.input_thresholds, self.inner_link.link().input_thresholds) - self.assign(self.outer_port.pullup_capable, self.inner_link.link().pullup_capable) - self.assign(self.outer_port.pulldown_capable, self.inner_link.link().pulldown_capable) + def __init__(self) -> None: + super().__init__() + + self.outer_port = self.Port( + DigitalBidir( + voltage_out=RangeExpr(), + current_draw=RangeExpr(), + voltage_limits=RangeExpr(), + current_limits=RangeExpr(), + output_thresholds=RangeExpr(), + input_thresholds=RangeExpr(), + pulldown_capable=BoolExpr(), + pullup_capable=BoolExpr(), + ) + ) + # TODO can we actually define something here? as a pseudoport, this doesn't have limits + self.inner_link = self.Port( + DigitalBidir( + voltage_limits=RangeExpr.ALL, + current_limits=RangeExpr.ALL, + pullup_capable=False, + pulldown_capable=False, # don't create a loop + _bridged_internal=True, + ) + ) + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.outer_port.voltage_out, self.inner_link.link().voltage) + self.assign(self.outer_port.current_draw, self.inner_link.link().current_drawn) + self.assign(self.outer_port.voltage_limits, self.inner_link.link().voltage_limits) + self.assign( + self.outer_port.current_limits, self.inner_link.link().current_limits + ) # TODO compensate for internal current draw + + self.assign(self.outer_port.output_thresholds, self.inner_link.link().output_thresholds) + self.assign(self.outer_port.input_thresholds, self.inner_link.link().input_thresholds) + self.assign(self.outer_port.pullup_capable, self.inner_link.link().pullup_capable) + self.assign(self.outer_port.pulldown_capable, self.inner_link.link().pulldown_capable) class DigitalBidirNotConnected(InternalBlock, Block): - """Not-connected dummy block for Digital bidir ports""" - def __init__(self) -> None: - super().__init__() - self.port = self.Port(DigitalBidir(), [InOut]) + """Not-connected dummy block for Digital bidir ports""" + + def __init__(self) -> None: + super().__init__() + self.port = self.Port(DigitalBidir(), [InOut]) class DigitalBidir(DigitalBase): - bridge_type = DigitalBidirBridge - not_connected_type = DigitalBidirNotConnected - - @staticmethod - def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink], - voltage_limit_abs: Optional[RangeLike] = None, - voltage_limit_tolerance: Optional[RangeLike] = None, - current_draw: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL, *, - input_threshold_factor: Optional[RangeLike] = None, - input_threshold_abs: Optional[RangeLike] = None, - output_threshold_factor: Optional[RangeLike] = None, - output_threshold_abs: Optional[RangeLike] = None, - pullup_capable: BoolLike = False, pulldown_capable: BoolLike = False) -> DigitalBidir: - supply_range = VoltageLink._supply_voltage_range(neg, pos) - if voltage_limit_abs is not None: - assert voltage_limit_tolerance is None - voltage_limit: RangeLike = voltage_limit_abs - elif voltage_limit_tolerance is not None: - voltage_limit = supply_range + RangeExpr._to_expr_type(voltage_limit_tolerance) - else: # generic default - voltage_limit = supply_range + RangeExpr._to_expr_type((-0.3, 0.3)) - - neg_base = GroundLink._voltage_range(neg).upper() - input_threshold: RangeLike - if input_threshold_factor is not None: - assert input_threshold_abs is None, "can only specify one input threshold type" - input_threshold_factor = RangeExpr._to_expr_type(input_threshold_factor) # TODO avoid internal functions? - input_range = VoltageLink._voltage_range(pos).lower() - neg_base - input_threshold = RangeExpr._to_expr_type(neg_base) + RangeExpr._to_expr_type(input_range) * input_threshold_factor - elif input_threshold_abs is not None: - assert input_threshold_factor is None, "can only specify one input threshold type" - input_threshold = RangeExpr._to_expr_type(input_threshold_abs) # TODO avoid internal functions? - else: # assumed ideal - input_threshold = RangeExpr.EMPTY - - output_threshold: RangeLike - if output_threshold_factor is not None: - assert output_threshold_abs is None, "can only specify one output threshold type" - output_threshold_factor = RangeExpr._to_expr_type(output_threshold_factor) - # use a pessimistic range - output_range = VoltageLink._voltage_range(pos).lower() - neg_base - output_threshold = RangeExpr._to_expr_type(neg_base) + output_threshold_factor * RangeExpr._to_expr_type(output_range) - elif output_threshold_abs is not None: - assert output_threshold_factor is None, "can only specify one output threshold type" - output_threshold = RangeExpr._to_expr_type(output_threshold_abs) # TODO avoid internal functions? - else: # assumed ideal - output_threshold = (neg_base, VoltageLink._voltage_range(pos).lower()) - - return DigitalBidir( # TODO get rid of to_expr_type w/ dedicated Range conversion - voltage_limits=voltage_limit, - current_draw=current_draw, - voltage_out=supply_range, - current_limits=current_limits, - input_thresholds=input_threshold, - output_thresholds=output_threshold, - pullup_capable=pullup_capable, pulldown_capable=pulldown_capable - ) - - def __init__(self, *, voltage_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO, - voltage_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL, - input_thresholds: RangeLike = RangeExpr.EMPTY, - output_thresholds: RangeLike = RangeExpr.ALL, - pullup_capable: BoolLike = False, - pulldown_capable: BoolLike = False, - _bridged_internal: BoolLike = False) -> None: - super().__init__() - self.voltage_limits: RangeExpr = self.Parameter(RangeExpr(voltage_limits)) - self.current_draw: RangeExpr = self.Parameter(RangeExpr(current_draw)) - self.voltage_out: RangeExpr = self.Parameter(RangeExpr(voltage_out)) - self.current_limits: RangeExpr = self.Parameter(RangeExpr(current_limits)) - self.input_thresholds: RangeExpr = self.Parameter(RangeExpr(input_thresholds)) - self.output_thresholds: RangeExpr = self.Parameter(RangeExpr(output_thresholds)) - - self.pullup_capable: BoolExpr = self.Parameter(BoolExpr(pullup_capable)) - self.pulldown_capable: BoolExpr = self.Parameter(BoolExpr(pulldown_capable)) - self._bridged_internal: BoolExpr = self.Parameter(BoolExpr(_bridged_internal)) + bridge_type = DigitalBidirBridge + not_connected_type = DigitalBidirNotConnected + + @staticmethod + def from_supply( + neg: Port[GroundLink], + pos: Port[VoltageLink], + voltage_limit_abs: Optional[RangeLike] = None, + voltage_limit_tolerance: Optional[RangeLike] = None, + current_draw: RangeLike = RangeExpr.ZERO, + current_limits: RangeLike = RangeExpr.ALL, + *, + input_threshold_factor: Optional[RangeLike] = None, + input_threshold_abs: Optional[RangeLike] = None, + output_threshold_factor: Optional[RangeLike] = None, + output_threshold_abs: Optional[RangeLike] = None, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + ) -> DigitalBidir: + supply_range = VoltageLink._supply_voltage_range(neg, pos) + if voltage_limit_abs is not None: + assert voltage_limit_tolerance is None + voltage_limit: RangeLike = voltage_limit_abs + elif voltage_limit_tolerance is not None: + voltage_limit = supply_range + RangeExpr._to_expr_type(voltage_limit_tolerance) + else: # generic default + voltage_limit = supply_range + RangeExpr._to_expr_type((-0.3, 0.3)) + + neg_base = GroundLink._voltage_range(neg).upper() + input_threshold: RangeLike + if input_threshold_factor is not None: + assert input_threshold_abs is None, "can only specify one input threshold type" + input_threshold_factor = RangeExpr._to_expr_type(input_threshold_factor) # TODO avoid internal functions? + input_range = VoltageLink._voltage_range(pos).lower() - neg_base + input_threshold = ( + RangeExpr._to_expr_type(neg_base) + RangeExpr._to_expr_type(input_range) * input_threshold_factor + ) + elif input_threshold_abs is not None: + assert input_threshold_factor is None, "can only specify one input threshold type" + input_threshold = RangeExpr._to_expr_type(input_threshold_abs) # TODO avoid internal functions? + else: # assumed ideal + input_threshold = RangeExpr.EMPTY + + output_threshold: RangeLike + if output_threshold_factor is not None: + assert output_threshold_abs is None, "can only specify one output threshold type" + output_threshold_factor = RangeExpr._to_expr_type(output_threshold_factor) + # use a pessimistic range + output_range = VoltageLink._voltage_range(pos).lower() - neg_base + output_threshold = RangeExpr._to_expr_type(neg_base) + output_threshold_factor * RangeExpr._to_expr_type( + output_range + ) + elif output_threshold_abs is not None: + assert output_threshold_factor is None, "can only specify one output threshold type" + output_threshold = RangeExpr._to_expr_type(output_threshold_abs) # TODO avoid internal functions? + else: # assumed ideal + output_threshold = (neg_base, VoltageLink._voltage_range(pos).lower()) + + return DigitalBidir( # TODO get rid of to_expr_type w/ dedicated Range conversion + voltage_limits=voltage_limit, + current_draw=current_draw, + voltage_out=supply_range, + current_limits=current_limits, + input_thresholds=input_threshold, + output_thresholds=output_threshold, + pullup_capable=pullup_capable, + pulldown_capable=pulldown_capable, + ) + + def __init__( + self, + *, + voltage_limits: RangeLike = RangeExpr.ALL, + current_draw: RangeLike = RangeExpr.ZERO, + voltage_out: RangeLike = RangeExpr.ZERO, + current_limits: RangeLike = RangeExpr.ALL, + input_thresholds: RangeLike = RangeExpr.EMPTY, + output_thresholds: RangeLike = RangeExpr.ALL, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + _bridged_internal: BoolLike = False, + ) -> None: + super().__init__() + self.voltage_limits: RangeExpr = self.Parameter(RangeExpr(voltage_limits)) + self.current_draw: RangeExpr = self.Parameter(RangeExpr(current_draw)) + self.voltage_out: RangeExpr = self.Parameter(RangeExpr(voltage_out)) + self.current_limits: RangeExpr = self.Parameter(RangeExpr(current_limits)) + self.input_thresholds: RangeExpr = self.Parameter(RangeExpr(input_thresholds)) + self.output_thresholds: RangeExpr = self.Parameter(RangeExpr(output_thresholds)) + + self.pullup_capable: BoolExpr = self.Parameter(BoolExpr(pullup_capable)) + self.pulldown_capable: BoolExpr = self.Parameter(BoolExpr(pulldown_capable)) + self._bridged_internal: BoolExpr = self.Parameter(BoolExpr(_bridged_internal)) class DigitalSingleSourceFake: - @staticmethod - @deprecated("use DigitalSource.sink_from_supply") - def low_from_supply(neg: Port[GroundLink], is_pulldown: bool = False) -> DigitalSource: - if not is_pulldown: - return DigitalSource.low_from_supply(neg) - else: - return DigitalSource.pulldown_from_supply(neg) - - @staticmethod - @deprecated("use DigitalSource.source_from_supply") - def high_from_supply(pos: Port[VoltageLink], is_pullup: bool = False) -> DigitalSource: - if not is_pullup: - return DigitalSource.high_from_supply(pos) - else: - return DigitalSource.pullup_from_supply(pos) - - def __call__(self, voltage_out: RangeLike = RangeExpr.ZERO, - output_thresholds: RangeLike = RangeExpr.ALL, *, - pullup_capable: BoolLike = False, - pulldown_capable: BoolLike = False, - low_signal_driver: BoolLike = False, - high_signal_driver: BoolLike = False) -> DigitalSource: - return DigitalSource( - voltage_out=voltage_out, - output_thresholds=output_thresholds, - pullup_capable=pullup_capable, - pulldown_capable=pulldown_capable, - low_driver=low_signal_driver, - high_driver=high_signal_driver - ) - - def empty(self) -> DigitalSource: - return DigitalSource.empty() + @staticmethod + @deprecated("use DigitalSource.sink_from_supply") + def low_from_supply(neg: Port[GroundLink], is_pulldown: bool = False) -> DigitalSource: + if not is_pulldown: + return DigitalSource.low_from_supply(neg) + else: + return DigitalSource.pulldown_from_supply(neg) + + @staticmethod + @deprecated("use DigitalSource.source_from_supply") + def high_from_supply(pos: Port[VoltageLink], is_pullup: bool = False) -> DigitalSource: + if not is_pullup: + return DigitalSource.high_from_supply(pos) + else: + return DigitalSource.pullup_from_supply(pos) + + def __call__( + self, + voltage_out: RangeLike = RangeExpr.ZERO, + output_thresholds: RangeLike = RangeExpr.ALL, + *, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + low_signal_driver: BoolLike = False, + high_signal_driver: BoolLike = False, + ) -> DigitalSource: + return DigitalSource( + voltage_out=voltage_out, + output_thresholds=output_thresholds, + pullup_capable=pullup_capable, + pulldown_capable=pulldown_capable, + low_driver=low_signal_driver, + high_driver=high_signal_driver, + ) + + def empty(self) -> DigitalSource: + return DigitalSource.empty() DigitalSingleSource = DigitalSingleSourceFake() diff --git a/edg/electronics_model/DvpPort.py b/edg/electronics_model/DvpPort.py index 9efded8af..15c6819f1 100644 --- a/edg/electronics_model/DvpPort.py +++ b/edg/electronics_model/DvpPort.py @@ -9,6 +9,7 @@ class Dvp8Link(Link): """DVP (Digital Video Port) camera link with 8-wide data connection. TODO: ideally this would be width-parameterized, but that core logic doesn't exist yet.""" + def __init__(self) -> None: super().__init__() self.host = self.Port(Dvp8Host(DigitalBidir.empty())) diff --git a/edg/electronics_model/GroundPort.py b/edg/electronics_model/GroundPort.py index a529b3438..bcfc05de5 100644 --- a/edg/electronics_model/GroundPort.py +++ b/edg/electronics_model/GroundPort.py @@ -18,8 +18,7 @@ class GroundLink(CircuitLink): @classmethod def _voltage_range(cls, port: Port[GroundLink]) -> RangeExpr: if isinstance(port, Ground): - return port.is_connected().then_else(port.link().voltage, - RangeExpr._to_expr_type(RangeExpr.ZERO)) + return port.is_connected().then_else(port.link().voltage, RangeExpr._to_expr_type(RangeExpr.ZERO)) elif isinstance(port, GroundReference): return port.voltage_out else: @@ -39,12 +38,13 @@ def contents(self) -> None: super().contents() self.description = DescriptionString( - "voltage: ", DescriptionString.FormatUnits(self.voltage, "V"), - " of limits: ", DescriptionString.FormatUnits(self.voltage_limits, "V")) + "voltage: ", + DescriptionString.FormatUnits(self.voltage, "V"), + " of limits: ", + DescriptionString.FormatUnits(self.voltage_limits, "V"), + ) - self.assign(self.voltage, self.ref.is_connected().then_else( - self.ref.voltage_out, (0, 0)*Volt - )) + self.assign(self.voltage, self.ref.is_connected().then_else(self.ref.voltage_out, (0, 0) * Volt)) self.assign(self.voltage_limits, self.gnds.intersection(lambda x: x.voltage_limits)) self.require(self.voltage_limits.contains(self.voltage), "overvoltage") @@ -63,37 +63,45 @@ def contents(self) -> None: self.assign(self.inner_link.voltage_out, self.outer_port.link().voltage) -class GroundAdapterVoltageSource(CircuitPortAdapter['VoltageSource']): +class GroundAdapterVoltageSource(CircuitPortAdapter["VoltageSource"]): def __init__(self) -> None: from .VoltagePorts import VoltageSource + super().__init__() self.src = self.Port(Ground()) - self.dst = self.Port(VoltageSource( - voltage_out=self.src.link().voltage, - )) + self.dst = self.Port( + VoltageSource( + voltage_out=self.src.link().voltage, + ) + ) -class GroundAdapterDigitalSource(CircuitPortAdapter['DigitalSource']): +class GroundAdapterDigitalSource(CircuitPortAdapter["DigitalSource"]): def __init__(self) -> None: from .DigitalPorts import DigitalSource + super().__init__() self.src = self.Port(Ground()) - self.dst = self.Port(DigitalSource( - voltage_out=self.src.link().voltage, - output_thresholds=(self.src.link().voltage.lower(), FloatExpr._to_expr_type(float('inf'))) - )) + self.dst = self.Port( + DigitalSource( + voltage_out=self.src.link().voltage, + output_thresholds=(self.src.link().voltage.lower(), FloatExpr._to_expr_type(float("inf"))), + ) + ) -class GroundAdapterAnalogSource(CircuitPortAdapter['AnalogSource']): +class GroundAdapterAnalogSource(CircuitPortAdapter["AnalogSource"]): def __init__(self) -> None: from .AnalogPort import AnalogSource super().__init__() self.src = self.Port(Ground()) - self.dst = self.Port(AnalogSource( - voltage_out=self.src.link().voltage, - signal_out=self.src.link().voltage, - )) + self.dst = self.Port( + AnalogSource( + voltage_out=self.src.link().voltage, + signal_out=self.src.link().voltage, + ) + ) class Ground(CircuitPort[GroundLink]): @@ -128,6 +136,8 @@ def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO) -> None: from deprecated import deprecated + + @deprecated("Use Ground() or GroundReference(...), Ground is no longer directioned") def GroundSource(*args: Any, **kwargs: Any) -> Ground: return Ground() diff --git a/edg/electronics_model/I2cPort.py b/edg/electronics_model/I2cPort.py index 8bec62d73..0474297b9 100644 --- a/edg/electronics_model/I2cPort.py +++ b/edg/electronics_model/I2cPort.py @@ -7,98 +7,105 @@ class I2cLink(Link): - """I2C connection, using terminology from the auhtoritative NXP specification at - https://www.nxp.com/docs/en/user-guide/UM10204.pdf. - """ - def __init__(self) -> None: - super().__init__() - - self.controller = self.Port(I2cController(DigitalBidir.empty())) - self.targets = self.Port(Vector(I2cTarget(DigitalBidir.empty()))) - - # in concept we should only have one pullup, but optional handling on non-vector ports is a mess - # and this breaks where we have to create a bridge, since the internal link has a disconnected pull port - # so this structurally allows multiple pullups, but an assertion checks that there aren't multiple - self.pull = self.Port(Vector(I2cPullupPort().empty()), optional=True) - - self.addresses = self.Parameter(ArrayIntExpr(self.targets.flatten(lambda x: x.addresses))) - - self.has_pull = self.Parameter(BoolExpr(self.pull.any_connected())) - - @override - def contents(self) -> None: - super().contents() - self.require(self.pull.any_connected() | self.controller.has_pullup) - self.require(self.pull.length() <= 1, "at most one pullup") - self.require(self.addresses.all_unique(), "conflicting addresses on I2C bus") - self.scl = self.connect(self.pull.map_extract(lambda device: device.scl), - self.controller.scl, self.targets.map_extract(lambda device: device.scl), - flatten=True) - self.sda = self.connect(self.pull.map_extract(lambda device: device.sda), - self.controller.sda, self.targets.map_extract(lambda device: device.sda), - flatten=True) + """I2C connection, using terminology from the auhtoritative NXP specification at + https://www.nxp.com/docs/en/user-guide/UM10204.pdf. + """ + + def __init__(self) -> None: + super().__init__() + + self.controller = self.Port(I2cController(DigitalBidir.empty())) + self.targets = self.Port(Vector(I2cTarget(DigitalBidir.empty()))) + + # in concept we should only have one pullup, but optional handling on non-vector ports is a mess + # and this breaks where we have to create a bridge, since the internal link has a disconnected pull port + # so this structurally allows multiple pullups, but an assertion checks that there aren't multiple + self.pull = self.Port(Vector(I2cPullupPort().empty()), optional=True) + + self.addresses = self.Parameter(ArrayIntExpr(self.targets.flatten(lambda x: x.addresses))) + + self.has_pull = self.Parameter(BoolExpr(self.pull.any_connected())) + + @override + def contents(self) -> None: + super().contents() + self.require(self.pull.any_connected() | self.controller.has_pullup) + self.require(self.pull.length() <= 1, "at most one pullup") + self.require(self.addresses.all_unique(), "conflicting addresses on I2C bus") + self.scl = self.connect( + self.pull.map_extract(lambda device: device.scl), + self.controller.scl, + self.targets.map_extract(lambda device: device.scl), + flatten=True, + ) + self.sda = self.connect( + self.pull.map_extract(lambda device: device.sda), + self.controller.sda, + self.targets.map_extract(lambda device: device.sda), + flatten=True, + ) class I2cPullupPort(Bundle[I2cLink]): - link_type = I2cLink + link_type = I2cLink - def __init__(self) -> None: - super().__init__() - self.scl = self.Port(DigitalSource(low_driver=False, high_driver=False, pullup_capable=True)) - self.sda = self.Port(DigitalSource(low_driver=False, high_driver=False, pullup_capable=True)) + def __init__(self) -> None: + super().__init__() + self.scl = self.Port(DigitalSource(low_driver=False, high_driver=False, pullup_capable=True)) + self.sda = self.Port(DigitalSource(low_driver=False, high_driver=False, pullup_capable=True)) class I2cController(Bundle[I2cLink]): - link_type = I2cLink + link_type = I2cLink - def __init__(self, model: Optional[DigitalBidir] = None, has_pullup: BoolLike = False) -> None: - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.scl = self.Port(DigitalSource.from_bidir(model)) - self.sda = self.Port(model) + def __init__(self, model: Optional[DigitalBidir] = None, has_pullup: BoolLike = False) -> None: + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.scl = self.Port(DigitalSource.from_bidir(model)) + self.sda = self.Port(model) - self.frequency = self.Parameter(RangeExpr(RangeExpr.ZERO)) - self.has_pullup = self.Parameter(BoolExpr(has_pullup)) + self.frequency = self.Parameter(RangeExpr(RangeExpr.ZERO)) + self.has_pullup = self.Parameter(BoolExpr(has_pullup)) class I2cTargetBridge(PortBridge): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.outer_port = self.Port(I2cTarget.empty()) - self.inner_link = self.Port(I2cController(DigitalBidir.empty(), self.outer_port.link().has_pull)) + self.outer_port = self.Port(I2cTarget.empty()) + self.inner_link = self.Port(I2cController(DigitalBidir.empty(), self.outer_port.link().has_pull)) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.outer_port.init_from(I2cTarget(DigitalBidir.empty(), self.inner_link.link().addresses)) + self.outer_port.init_from(I2cTarget(DigitalBidir.empty(), self.inner_link.link().addresses)) - # this duplicates DigitalBidirBridge but mixing in the pullup - self.scl_bridge = self.Block(DigitalSinkBridge()) - self.connect(self.outer_port.scl, self.scl_bridge.outer_port) - self.connect(self.scl_bridge.inner_link, self.inner_link.scl) + # this duplicates DigitalBidirBridge but mixing in the pullup + self.scl_bridge = self.Block(DigitalSinkBridge()) + self.connect(self.outer_port.scl, self.scl_bridge.outer_port) + self.connect(self.scl_bridge.inner_link, self.inner_link.scl) - self.sda_bridge = self.Block(DigitalBidirBridge()) - self.connect(self.outer_port.sda, self.sda_bridge.outer_port) - self.connect(self.sda_bridge.inner_link, self.inner_link.sda) + self.sda_bridge = self.Block(DigitalBidirBridge()) + self.connect(self.outer_port.sda, self.sda_bridge.outer_port) + self.connect(self.sda_bridge.inner_link, self.inner_link.sda) class I2cTarget(Bundle[I2cLink]): - link_type = I2cLink - bridge_type = I2cTargetBridge - - def __init__(self, model: Optional[DigitalBidir] = None, addresses: ArrayIntLike = []) -> None: - """Addresses specified excluding the R/W bit (as a 7-bit number, as directly used by Arduino)""" - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.scl = self.Port(DigitalSink.from_bidir(model)) - self.sda = self.Port(model) - - self.frequency_limit = self.Parameter(RangeExpr(RangeExpr.ALL)) # range of acceptable frequencies - self.addresses = self.Parameter(ArrayIntExpr(addresses)) + link_type = I2cLink + bridge_type = I2cTargetBridge + + def __init__(self, model: Optional[DigitalBidir] = None, addresses: ArrayIntLike = []) -> None: + """Addresses specified excluding the R/W bit (as a 7-bit number, as directly used by Arduino)""" + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.scl = self.Port(DigitalSink.from_bidir(model)) + self.sda = self.Port(model) + + self.frequency_limit = self.Parameter(RangeExpr(RangeExpr.ALL)) # range of acceptable frequencies + self.addresses = self.Parameter(ArrayIntExpr(addresses)) # legacy names diff --git a/edg/electronics_model/I2sPort.py b/edg/electronics_model/I2sPort.py index edce955bf..d3f88aa06 100644 --- a/edg/electronics_model/I2sPort.py +++ b/edg/electronics_model/I2sPort.py @@ -7,41 +7,43 @@ class I2sLink(Link): - def __init__(self) -> None: - super().__init__() - self.controller = self.Port(I2sController(DigitalBidir.empty())) - self.target_receiver = self.Port(I2sTargetReceiver(DigitalSink.empty())) - # TODO: multiple receivers, target transmitters, eg microphones + def __init__(self) -> None: + super().__init__() + self.controller = self.Port(I2sController(DigitalBidir.empty())) + self.target_receiver = self.Port(I2sTargetReceiver(DigitalSink.empty())) + # TODO: multiple receivers, target transmitters, eg microphones - @override - def contents(self) -> None: - super().contents() - self.sck = self.connect(self.controller.sck, self.target_receiver.sck) - self.ws = self.connect(self.controller.ws, self.target_receiver.ws) - self.sd = self.connect(self.controller.sd, self.target_receiver.sd) + @override + def contents(self) -> None: + super().contents() + self.sck = self.connect(self.controller.sck, self.target_receiver.sck) + self.ws = self.connect(self.controller.ws, self.target_receiver.ws) + self.sd = self.connect(self.controller.sd, self.target_receiver.sd) class I2sController(Bundle[I2sLink]): - """Controller is both controller (drives SCK and WS lines) and transmitter (SD is output)""" - link_type = I2sLink + """Controller is both controller (drives SCK and WS lines) and transmitter (SD is output)""" - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.sck = self.Port(DigitalSource.from_bidir(model)) - self.ws = self.Port(DigitalSource.from_bidir(model)) - self.sd = self.Port(model) # bidirectional + link_type = I2sLink + + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.sck = self.Port(DigitalSource.from_bidir(model)) + self.ws = self.Port(DigitalSource.from_bidir(model)) + self.sd = self.Port(model) # bidirectional class I2sTargetReceiver(Bundle[I2sLink]): - """Target means SCK and WS are inputs, receiver means SD is input""" - link_type = I2sLink - - def __init__(self, model: Optional[DigitalSink] = None) -> None: - super().__init__() - if model is None: - model = DigitalSink() # ideal by default - self.sck = self.Port(model) - self.ws = self.Port(model) - self.sd = self.Port(model) + """Target means SCK and WS are inputs, receiver means SD is input""" + + link_type = I2sLink + + def __init__(self, model: Optional[DigitalSink] = None) -> None: + super().__init__() + if model is None: + model = DigitalSink() # ideal by default + self.sck = self.Port(model) + self.ws = self.Port(model) + self.sd = self.Port(model) diff --git a/edg/electronics_model/KiCadImportableBlock.py b/edg/electronics_model/KiCadImportableBlock.py index c76b1609d..17087d24a 100644 --- a/edg/electronics_model/KiCadImportableBlock.py +++ b/edg/electronics_model/KiCadImportableBlock.py @@ -8,25 +8,26 @@ @non_library class KiCadImportableBlock(Block): - """A mixin for a Block that also defines a pin mapping for KiCad symbol(s), - allowing this to be used in a schematic. - The Block still must be instantiated via HDL, but the connectivity can be defined by a schematic.""" + """A mixin for a Block that also defines a pin mapping for KiCad symbol(s), + allowing this to be used in a schematic. + The Block still must be instantiated via HDL, but the connectivity can be defined by a schematic.""" - @abstractmethod - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - """Returns the symbol pin number to Block Port correspondence, for KiCad schematic import.""" - raise NotImplementedError # implement me + @abstractmethod + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + """Returns the symbol pin number to Block Port correspondence, for KiCad schematic import.""" + raise NotImplementedError # implement me @non_library class KiCadInstantiableBlock(KiCadImportableBlock): - """A mixin for a Block that allows the block to be instantiated from a KiCad symbol - by parsing the symbol and properties. - For obvious reasons this must also define symbol_pinning in KicadImportableBlocm""" - BlockSelfType = TypeVar('BlockSelfType', bound=KiCadImportableBlock) - - @classmethod - @abstractmethod - def block_from_symbol(cls: Type[BlockSelfType], symbol_name: str, properties: Mapping[str, str]) -> BlockSelfType: - """Creates an instance of this block given a symbol name and its properties, for KiCad schematic import.""" - raise NotImplementedError # implement me + """A mixin for a Block that allows the block to be instantiated from a KiCad symbol + by parsing the symbol and properties. + For obvious reasons this must also define symbol_pinning in KicadImportableBlocm""" + + BlockSelfType = TypeVar("BlockSelfType", bound=KiCadImportableBlock) + + @classmethod + @abstractmethod + def block_from_symbol(cls: Type[BlockSelfType], symbol_name: str, properties: Mapping[str, str]) -> BlockSelfType: + """Creates an instance of this block given a symbol name and its properties, for KiCad schematic import.""" + raise NotImplementedError # implement me diff --git a/edg/electronics_model/KiCadSchematicBlock.py b/edg/electronics_model/KiCadSchematicBlock.py index 36c3a4831..b1037e8ed 100644 --- a/edg/electronics_model/KiCadSchematicBlock.py +++ b/edg/electronics_model/KiCadSchematicBlock.py @@ -10,19 +10,29 @@ from .VoltagePorts import CircuitPort from .PassivePort import Passive from .KiCadImportableBlock import KiCadInstantiableBlock, KiCadImportableBlock -from .KiCadSchematicParser import KiCadSchematic, KiCadPin, KiCadLabel, KiCadGlobalLabel, KiCadHierarchicalLabel, \ - KiCadPowerLabel, KiCadSymbol, KiCadLibSymbol +from .KiCadSchematicParser import ( + KiCadSchematic, + KiCadPin, + KiCadLabel, + KiCadGlobalLabel, + KiCadHierarchicalLabel, + KiCadPowerLabel, + KiCadSymbol, + KiCadLibSymbol, +) @non_library class KiCadBlackboxBase(InternalBlock): """Abstract class for black box handlers, which parses a KiCad symbol and creates the corresponding Block.""" - BlackboxSelfType = TypeVar('BlackboxSelfType', bound='KiCadBlackboxBase') + + BlackboxSelfType = TypeVar("BlackboxSelfType", bound="KiCadBlackboxBase") @classmethod @abstractmethod - def block_from_symbol(cls: Type[BlackboxSelfType], symbol: KiCadSymbol, lib: KiCadLibSymbol) -> \ - Tuple[BlackboxSelfType, Callable[[BlackboxSelfType], Mapping[str, BasePort]]]: + def block_from_symbol( + cls: Type[BlackboxSelfType], symbol: KiCadSymbol, lib: KiCadLibSymbol + ) -> Tuple[BlackboxSelfType, Callable[[BlackboxSelfType], Mapping[str, BasePort]]]: """Creates a blackbox block from a schematic symbol. Returns the block template and a function that given the block, returns mapping from the schematic pin name to the associated port.""" ... @@ -32,22 +42,37 @@ class KiCadBlackbox(KiCadBlackboxBase, FootprintBlock, GeneratorBlock, InternalB """A footprint block that is fully defined (both value fields and structural pins) by its argument parameters and has all passive ports. """ + @classmethod @override - def block_from_symbol(cls, symbol: KiCadSymbol, lib: KiCadLibSymbol) -> \ - Tuple['KiCadBlackbox', Callable[['KiCadBlackbox'], Mapping[str, BasePort]]]: + def block_from_symbol( + cls, symbol: KiCadSymbol, lib: KiCadLibSymbol + ) -> Tuple["KiCadBlackbox", Callable[["KiCadBlackbox"], Mapping[str, BasePort]]]: pin_numbers = [pin.number for pin in lib.pins] - refdes_prefix = symbol.properties.get('Refdes Prefix', symbol.refdes.rstrip('0123456789?')) + refdes_prefix = symbol.properties.get("Refdes Prefix", symbol.refdes.rstrip("0123456789?")) block_model = KiCadBlackbox( - pin_numbers, refdes_prefix, symbol.properties['Footprint'], - kicad_part=symbol.lib, kicad_value=symbol.properties.get('Value', ''), - kicad_datasheet=symbol.properties.get('Datasheet', '')) + pin_numbers, + refdes_prefix, + symbol.properties["Footprint"], + kicad_part=symbol.lib, + kicad_value=symbol.properties.get("Value", ""), + kicad_datasheet=symbol.properties.get("Datasheet", ""), + ) + def block_pinning(block: KiCadBlackbox) -> Mapping[str, BasePort]: return {pin: block.ports.request(pin) for pin in pin_numbers} + return block_model, block_pinning - def __init__(self, kicad_pins: ArrayStringLike, kicad_refdes_prefix: StringLike, kicad_footprint: StringLike, - kicad_part: StringLike, kicad_value: StringLike, kicad_datasheet: StringLike): + def __init__( + self, + kicad_pins: ArrayStringLike, + kicad_refdes_prefix: StringLike, + kicad_footprint: StringLike, + kicad_part: StringLike, + kicad_value: StringLike, + kicad_datasheet: StringLike, + ): super().__init__() self.ports = self.Port(Vector(Passive()), optional=True) self.kicad_refdes_prefix = self.ArgParameter(kicad_refdes_prefix) @@ -62,11 +87,16 @@ def __init__(self, kicad_pins: ArrayStringLike, kicad_refdes_prefix: StringLike, @override def generate(self) -> None: super().generate() - mapping = {pin_name: self.ports.append_elt(Passive(), pin_name) - for pin_name in self.get(self.kicad_pins)} + mapping = {pin_name: self.ports.append_elt(Passive(), pin_name) for pin_name in self.get(self.kicad_pins)} self.ports.defined() - self.footprint(self.kicad_refdes_prefix, self.kicad_footprint, mapping, - part=self.kicad_part, value=self.kicad_value, datasheet=self.kicad_datasheet) + self.footprint( + self.kicad_refdes_prefix, + self.kicad_footprint, + mapping, + part=self.kicad_part, + value=self.kicad_value, + datasheet=self.kicad_datasheet, + ) @non_library @@ -90,27 +120,38 @@ class KiCadSchematicBlock(Block): Passive-typed ports in the schematic can be converted to the target port model via the conversions mapping. This Block's interface (ports, parameters) must remain defined in HDL, to support static analysis tools.""" + @staticmethod - def _port_from_pin(pin: KiCadPin, mapping: Mapping[str, BasePort], - conversions: Mapping[str, CircuitPort]) -> BasePort: + def _port_from_pin( + pin: KiCadPin, mapping: Mapping[str, BasePort], conversions: Mapping[str, CircuitPort] + ) -> BasePort: """Returns the Port from a symbol's pin, using the provided mapping and applying conversions as needed.""" from .PassivePort import Passive if pin.pin_number in mapping and pin.pin_name in mapping and pin.pin_number != pin.pin_name: - raise ValueError(f"ambiguous pinning for {pin.refdes}.{pin.pin_number}, " - f"mapping defined for both number ${pin.pin_number} and name ${pin.pin_name}") + raise ValueError( + f"ambiguous pinning for {pin.refdes}.{pin.pin_number}, " + f"mapping defined for both number ${pin.pin_number} and name ${pin.pin_name}" + ) elif pin.pin_number in mapping: port = mapping[pin.pin_number] elif pin.pin_name in mapping: port = mapping[pin.pin_name] else: - raise ValueError(f"no pinning for {pin.refdes}.{pin.pin_number}, " - f"no mapping defined for either name ${pin.pin_name} or number ${pin.pin_number}") - - if f"{pin.refdes}.{pin.pin_number}" in conversions and f"{pin.refdes}.{pin.pin_name}" in conversions\ - and pin.pin_number != pin.pin_name: - raise ValueError(f"ambiguous conversion for {pin.refdes}.{pin.pin_number}, " - f"mapping defined for both number ${pin.pin_number} and name ${pin.pin_name}") + raise ValueError( + f"no pinning for {pin.refdes}.{pin.pin_number}, " + f"no mapping defined for either name ${pin.pin_name} or number ${pin.pin_number}" + ) + + if ( + f"{pin.refdes}.{pin.pin_number}" in conversions + and f"{pin.refdes}.{pin.pin_name}" in conversions + and pin.pin_number != pin.pin_name + ): + raise ValueError( + f"ambiguous conversion for {pin.refdes}.{pin.pin_number}, " + f"mapping defined for both number ${pin.pin_number} and name ${pin.pin_name}" + ) elif f"{pin.refdes}.{pin.pin_number}" in conversions: conversion: Optional[CircuitPort] = conversions[f"{pin.refdes}.{pin.pin_number}"] elif f"{pin.refdes}.{pin.pin_name}" in conversions: @@ -119,14 +160,16 @@ def _port_from_pin(pin: KiCadPin, mapping: Mapping[str, BasePort], conversion = None if conversion is not None: - assert isinstance(port, Passive),\ - f"conversion only allowed on Passive ports, got {pin.refdes}.{pin.pin_number}: {port.__class__.__name__}" + assert isinstance( + port, Passive + ), f"conversion only allowed on Passive ports, got {pin.refdes}.{pin.pin_number}: {port.__class__.__name__}" port = port.adapt_to(conversion) return port def _port_from_path(self, path: str) -> Optional[BasePort]: """Returns the corresponding Port given a path string, recursing into bundles as needed""" + def inner(root: Any, components: List[str]) -> Optional[BasePort]: if not components: if isinstance(root, BasePort): @@ -137,7 +180,8 @@ def inner(root: Any, components: List[str]) -> Optional[BasePort]: return inner(getattr(root, components[0]), components[1:]) else: return None - return inner(self, path.split('.')) + + return inner(self, path.split(".")) """ Import the schematic file specified by the filepath. @@ -152,69 +196,81 @@ def inner(root: Any, components: List[str]) -> Optional[BasePort]: ideal boundary port type. This can be used in conjunction with conversions, though conversions take priority. """ - def import_kicad(self, filepath: str, locals: Mapping[str, Any] = {}, - *, nodes: Mapping[str, Optional[BasePort]] = {}, conversions: Mapping[str, CircuitPort] = {}, - auto_adapt: bool = False) -> None: + + def import_kicad( + self, + filepath: str, + locals: Mapping[str, Any] = {}, + *, + nodes: Mapping[str, Optional[BasePort]] = {}, + conversions: Mapping[str, CircuitPort] = {}, + auto_adapt: bool = False, + ) -> None: # ideally SYMBOL_MAP would be a class variable, but this causes a import loop with Opamp, # so declaring it here causes it to reference Opamp lazily from ..abstract_parts import Resistor, Capacitor, Opamp + SYMBOL_MAP: Mapping[str, Type[KiCadInstantiableBlock]] = { - 'Device:R': Resistor, - 'Device:R_Small': Resistor, - 'Device:C': Capacitor, - 'Device:C_Small': Capacitor, - 'Device:C_Polarized': Capacitor, - 'Device:C_Polarized_Small': Capacitor, - 'Simulation_SPICE:OPAMP': Opamp, - 'edg_importable:Opamp': Opamp, + "Device:R": Resistor, + "Device:R_Small": Resistor, + "Device:C": Capacitor, + "Device:C_Small": Capacitor, + "Device:C_Polarized": Capacitor, + "Device:C_Polarized_Small": Capacitor, + "Simulation_SPICE:OPAMP": Opamp, + "edg_importable:Opamp": Opamp, } - with open(filepath, "r", encoding='utf-8') as file: + with open(filepath, "r", encoding="utf-8") as file: file_data = file.read() sch = KiCadSchematic(file_data) blocks_pins: Dict[str, Mapping[str, BasePort]] = {} for symbol in sch.symbols: - if 'Footprint' in symbol.properties and symbol.properties['Footprint']: # footprints are blackboxed + if "Footprint" in symbol.properties and symbol.properties["Footprint"]: # footprints are blackboxed handler: Type[KiCadBlackboxBase] = KiCadBlackbox # default - if 'edg_blackbox' in symbol.properties: - handler_name = symbol.properties['edg_blackbox'] + if "edg_blackbox" in symbol.properties: + handler_name = symbol.properties["edg_blackbox"] container_globals = inspect.stack()[1][0].f_globals - assert handler_name in container_globals, \ - f"edg_blackbox handler {handler_name} must be imported into current global scope" + assert ( + handler_name in container_globals + ), f"edg_blackbox handler {handler_name} must be imported into current global scope" handler = container_globals[handler_name] - assert issubclass(handler, KiCadBlackboxBase), \ - f"edg_blackbox handler {handler_name} must subclass KiCadBlackboxBase" + assert issubclass( + handler, KiCadBlackboxBase + ), f"edg_blackbox handler {handler_name} must subclass KiCadBlackboxBase" - block_model, block_pinning_creator = handler.block_from_symbol( - symbol, sch.lib_symbols[symbol.lib_ref]) + block_model, block_pinning_creator = handler.block_from_symbol(symbol, sch.lib_symbols[symbol.lib_ref]) blackbox_block = self.Block(block_model) block_pinning = block_pinning_creator(blackbox_block) setattr(self, symbol.refdes, blackbox_block) elif hasattr(self, symbol.refdes): # sub-block defined in the Python Block, schematic only for connections - assert not symbol.properties['Value'] or symbol.properties['Value'] == '~',\ - f"{symbol.refdes} has both code block and non-empty value" + assert ( + not symbol.properties["Value"] or symbol.properties["Value"] == "~" + ), f"{symbol.refdes} has both code block and non-empty value" block = getattr(self, symbol.refdes) block_pinning = block.symbol_pinning(symbol.lib) assert isinstance(block, KiCadImportableBlock), f"{symbol.refdes} not a KiCadImportableBlock" - elif symbol.properties['Value'].startswith('#'): # sub-block with inline Python in the value - inline_code = symbol.properties['Value'][1:] + elif symbol.properties["Value"].startswith("#"): # sub-block with inline Python in the value + inline_code = symbol.properties["Value"][1:] - value_suffixes = [int(name[5:]) for name in symbol.properties.keys() - if name.startswith('Value') and len(name) > 5] + value_suffixes = [ + int(name[5:]) for name in symbol.properties.keys() if name.startswith("Value") and len(name) > 5 + ] if len(value_suffixes): # support fake-multiline values with Value2, Value3, ... - assert min(value_suffixes) == 2, "additional Values must start at 2" - max_value = max(value_suffixes) - for suffix in range(2, max_value + 1): # starts at Value2 - assert f'Value{suffix}' in symbol.properties, f"missing Value{suffix} of Value{max_value}" - inline_code += '\n' + symbol.properties[f'Value{suffix}'] + assert min(value_suffixes) == 2, "additional Values must start at 2" + max_value = max(value_suffixes) + for suffix in range(2, max_value + 1): # starts at Value2 + assert f"Value{suffix}" in symbol.properties, f"missing Value{suffix} of Value{max_value}" + inline_code += "\n" + symbol.properties[f"Value{suffix}"] # use the caller's globals, since this needs to reflect the caller's imports block_model = eval(inline_code, inspect.stack()[1][0].f_globals, locals) block = self.Block(block_model) - assert isinstance(block, KiCadImportableBlock), \ - f"block {block} created by {inline_code} not KicadImportableBlock" + assert isinstance( + block, KiCadImportableBlock + ), f"block {block} created by {inline_code} not KicadImportableBlock" block_pinning = block.symbol_pinning(symbol.lib) setattr(self, symbol.refdes, block) elif symbol.lib in SYMBOL_MAP: # sub-block with code to parse the value @@ -228,8 +284,7 @@ def import_kicad(self, filepath: str, locals: Mapping[str, Any] = {}, blocks_pins[symbol.refdes] = block_pinning for net in sch.nets: - net_ports = [self._port_from_pin(pin, blocks_pins[pin.refdes], conversions) - for pin in net.pins] + net_ports = [self._port_from_pin(pin, blocks_pins[pin.refdes], conversions) for pin in net.pins] boundary_ports: List[Tuple[BasePort, str]] = [] net_label_names = set() port_label_names = set() @@ -244,8 +299,9 @@ def import_kicad(self, filepath: str, locals: Mapping[str, Any] = {}, for global_label_name in port_label_names: global_label_port = self._port_from_path(global_label_name) if global_label_name in nodes: # nodes if needed - assert not hasattr(self, global_label_name), \ - f"global label {global_label_name} has both node and boundary port" + assert not hasattr( + self, global_label_name + ), f"global label {global_label_name} has both node and boundary port" node = nodes[global_label_name] if node is not None: boundary_ports.append((node, global_label_name)) @@ -257,13 +313,17 @@ def import_kicad(self, filepath: str, locals: Mapping[str, Any] = {}, connection = self.connect(*net_ports) can_adapt = net_ports and all([isinstance(x, Passive) for x in net_ports]) - for (boundary_port, boundary_port_name) in boundary_ports: # generate adapters as needed, port by port + for boundary_port, boundary_port_name in boundary_ports: # generate adapters as needed, port by port if boundary_port_name in conversions: assert can_adapt, "conversion to boundary port only allowed for Passive ports" adapted = cast(Passive, net_ports[0]).adapt_to(conversions[boundary_port_name]) self.connect(adapted, boundary_port) - elif auto_adapt and can_adapt and isinstance(boundary_port, CircuitPort) and \ - not isinstance(boundary_port, Passive): + elif ( + auto_adapt + and can_adapt + and isinstance(boundary_port, CircuitPort) + and not isinstance(boundary_port, Passive) + ): adapted = cast(Passive, net_ports[0]).adapt_to(boundary_port.__class__()) self.connect(adapted, boundary_port) else: diff --git a/edg/electronics_model/KiCadSchematicParser.py b/edg/electronics_model/KiCadSchematicParser.py index c318e5bb2..4ff904829 100644 --- a/edg/electronics_model/KiCadSchematicParser.py +++ b/edg/electronics_model/KiCadSchematicParser.py @@ -12,369 +12,390 @@ MIN_GRID = 1.27 -TestCastType = TypeVar('TestCastType') +TestCastType = TypeVar("TestCastType") + + def test_cast(x: Any, tpe: Type[TestCastType]) -> TestCastType: - """Combination of (dynamic) isinstance test and static typing cast.""" - assert isinstance(x, tpe), f"got {x} of type {type(x)}, expected {tpe}" - return x + """Combination of (dynamic) isinstance test and static typing cast.""" + assert isinstance(x, tpe), f"got {x} of type {type(x)}, expected {tpe}" + return x + def extract_only(x: List[TestCastType]) -> TestCastType: - """Asserts the input list only has one element, and returns it.""" - assert len(x) == 1 - return x[0] + """Asserts the input list only has one element, and returns it.""" + assert len(x) == 1 + return x[0] + def group_by_car(elts: List[Any]) -> Dict[Any, List[List[Any]]]: - """Groups a list of elements by each element's car (its first element). - Discards elements that have a non-symbol car or elements that are just a symbol.""" - out_dict: Dict[Any, List[List[Any]]] = {} - for elt in elts: - if isinstance(elt, list) and isinstance(elt[0], sexpdata.Symbol): - out_dict.setdefault(elt[0].value(), []).append(elt) - # otherwise discard - return out_dict + """Groups a list of elements by each element's car (its first element). + Discards elements that have a non-symbol car or elements that are just a symbol.""" + out_dict: Dict[Any, List[List[Any]]] = {} + for elt in elts: + if isinstance(elt, list) and isinstance(elt[0], sexpdata.Symbol): + out_dict.setdefault(elt[0].value(), []).append(elt) + # otherwise discard + return out_dict + PointType = Tuple[float, float] -def parse_xy(sexp: List[Any], expected_car: str = 'xy') -> PointType: - """Given a sexp of the form (xy, x, y) (for x, y float), returns (x, y). - X and Y are returned as part of an integer grid and rounded so points line up exactly.""" - assert len(sexp) == 3 - assert sexp[0] == sexpdata.Symbol(expected_car) - return (round(float(sexp[1]) / MIN_GRID), round(float(sexp[2]) / MIN_GRID)) - -def parse_at(sexp: List[Any], expected_car: str = 'at') -> Tuple[float, float, float]: - """Given a sexp of the form (at, x, y, r) (for x, y, r float), returns (x, y, r). - X and Y are returned as part of an integer grid and rounded so points line up exactly.""" - assert len(sexp) == 4 - assert sexp[0] == sexpdata.Symbol(expected_car) - return (round(float(sexp[1]) / MIN_GRID), round(float(sexp[2]) / MIN_GRID), float(sexp[3])) + + +def parse_xy(sexp: List[Any], expected_car: str = "xy") -> PointType: + """Given a sexp of the form (xy, x, y) (for x, y float), returns (x, y). + X and Y are returned as part of an integer grid and rounded so points line up exactly.""" + assert len(sexp) == 3 + assert sexp[0] == sexpdata.Symbol(expected_car) + return (round(float(sexp[1]) / MIN_GRID), round(float(sexp[2]) / MIN_GRID)) + + +def parse_at(sexp: List[Any], expected_car: str = "at") -> Tuple[float, float, float]: + """Given a sexp of the form (at, x, y, r) (for x, y, r float), returns (x, y, r). + X and Y are returned as part of an integer grid and rounded so points line up exactly.""" + assert len(sexp) == 4 + assert sexp[0] == sexpdata.Symbol(expected_car) + return (round(float(sexp[1]) / MIN_GRID), round(float(sexp[2]) / MIN_GRID), float(sexp[3])) + def parse_symbol(sexp: Any) -> str: - """Asserts sexp is a Symbol and returns its value.""" - assert isinstance(sexp, sexpdata.Symbol) - return cast(str, sexp.value()) + """Asserts sexp is a Symbol and returns its value.""" + assert isinstance(sexp, sexpdata.Symbol) + return cast(str, sexp.value()) class KiCadLibPin: - """Pin in a library symbol""" - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.number} @ {self.pos})" + """Pin in a library symbol""" + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.number} @ {self.pos})" - def __init__(self, sexp: List[Any]): - assert parse_symbol(sexp[0]) == 'pin' - sexp_dict = group_by_car(sexp) - self.pos = parse_at(extract_only(sexp_dict['at'])) - self.name = test_cast(extract_only(sexp_dict['name'])[1], str) - self.number = test_cast(extract_only(sexp_dict['number'])[1], str) + def __init__(self, sexp: List[Any]): + assert parse_symbol(sexp[0]) == "pin" + sexp_dict = group_by_car(sexp) + self.pos = parse_at(extract_only(sexp_dict["at"])) + self.name = test_cast(extract_only(sexp_dict["name"])[1], str) + self.number = test_cast(extract_only(sexp_dict["number"])[1], str) class KiCadLibSymbol: - """Symbol in a library""" - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.name})" - - def __init__(self, sexp: List[Any]): - assert parse_symbol(sexp[0]) == 'symbol' - self.name = test_cast(sexp[1], str) - sexp_dict = group_by_car(sexp) - self.properties: Dict[str, str] = {test_cast(prop[1], str): test_cast(prop[2], str) - for prop in sexp_dict.get('property', [])} - symbol_elts = itertools.chain(*[symbol_sexp[2:] # discard 'symbol' and the name - for symbol_sexp in sexp_dict.get('symbol', [])]) - symbol_elts_dict = group_by_car(list(symbol_elts)) - self.pins = [KiCadLibPin(pin_sexp) for pin_sexp in symbol_elts_dict.get('pin', [])] - self.is_power = 'power' in sexp_dict + """Symbol in a library""" + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.name})" + + def __init__(self, sexp: List[Any]): + assert parse_symbol(sexp[0]) == "symbol" + self.name = test_cast(sexp[1], str) + sexp_dict = group_by_car(sexp) + self.properties: Dict[str, str] = { + test_cast(prop[1], str): test_cast(prop[2], str) for prop in sexp_dict.get("property", []) + } + symbol_elts = itertools.chain( + *[symbol_sexp[2:] for symbol_sexp in sexp_dict.get("symbol", [])] # discard 'symbol' and the name + ) + symbol_elts_dict = group_by_car(list(symbol_elts)) + self.pins = [KiCadLibPin(pin_sexp) for pin_sexp in symbol_elts_dict.get("pin", [])] + self.is_power = "power" in sexp_dict class KiCadWire: - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.pt1}, {self.pt2})" + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.pt1}, {self.pt2})" - def __init__(self, sexp: List[Any]): - assert parse_symbol(sexp[0]) == 'wire' - sexp_dict = group_by_car(sexp) - pts = extract_only(sexp_dict['pts']) - assert len(pts) == 3 # pts, pt0, pt1 - self.pt1 = parse_xy(pts[1], 'xy') - self.pt2 = parse_xy(pts[2], 'xy') + def __init__(self, sexp: List[Any]): + assert parse_symbol(sexp[0]) == "wire" + sexp_dict = group_by_car(sexp) + pts = extract_only(sexp_dict["pts"]) + assert len(pts) == 3 # pts, pt0, pt1 + self.pt1 = parse_xy(pts[1], "xy") + self.pt2 = parse_xy(pts[2], "xy") class KiCadTunnel: - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.name} @ {self.pt})" + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.name} @ {self.pt})" - def __init__(self) -> None: - self.name: str - self.pt: PointType + def __init__(self) -> None: + self.name: str + self.pt: PointType class KiCadBaseLabel(KiCadTunnel): - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.name} @ {self.pt})" + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.name} @ {self.pt})" - def __init__(self, sexp: List[Any]): - super().__init__() - sexp_dict = group_by_car(sexp) - self.name = test_cast(sexp[1], str) - self.pos = parse_at(extract_only(sexp_dict['at'])) - self.pt = (self.pos[0], self.pos[1]) # version without rotation + def __init__(self, sexp: List[Any]): + super().__init__() + sexp_dict = group_by_car(sexp) + self.name = test_cast(sexp[1], str) + self.pos = parse_at(extract_only(sexp_dict["at"])) + self.pt = (self.pos[0], self.pos[1]) # version without rotation class KiCadLabel(KiCadBaseLabel): - def __init__(self, sexp: List[Any]): - super().__init__(sexp) - assert parse_symbol(sexp[0]) == 'label' + def __init__(self, sexp: List[Any]): + super().__init__(sexp) + assert parse_symbol(sexp[0]) == "label" class KiCadGlobalLabel(KiCadBaseLabel): - def __init__(self, sexp: List[Any]): - super().__init__(sexp) - assert parse_symbol(sexp[0]) == 'global_label' + def __init__(self, sexp: List[Any]): + super().__init__(sexp) + assert parse_symbol(sexp[0]) == "global_label" class KiCadHierarchicalLabel(KiCadBaseLabel): - def __init__(self, sexp: List[Any]): - super().__init__(sexp) - assert parse_symbol(sexp[0]) == 'hierarchical_label' + def __init__(self, sexp: List[Any]): + super().__init__(sexp) + assert parse_symbol(sexp[0]) == "hierarchical_label" class KiCadPowerLabel(KiCadTunnel): # not really a label, but behaves like a label - def __init__(self, name: str, pt: PointType): - super().__init__() - self.name = name - self.pt = pt + def __init__(self, name: str, pt: PointType): + super().__init__() + self.name = name + self.pt = pt class KiCadMarker: - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.pt})" + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.pt})" - def __init__(self) -> None: - self.pt: PointType + def __init__(self) -> None: + self.pt: PointType class KiCadNoConnect(KiCadMarker): - def __init__(self, sexp: List[Any]): - super().__init__() - sexp_dict = group_by_car(sexp) - assert parse_symbol(sexp[0]) == 'no_connect' - self.pt = parse_xy(extract_only(sexp_dict['at']), 'at') + def __init__(self, sexp: List[Any]): + super().__init__() + sexp_dict = group_by_car(sexp) + assert parse_symbol(sexp[0]) == "no_connect" + self.pt = parse_xy(extract_only(sexp_dict["at"]), "at") class KiCadSymbol: - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.refdes}, {self.lib} @ {self.pos})" - - def __init__(self, sexp: List[Any]): - assert parse_symbol(sexp[0]) == 'symbol' - sexp_dict = group_by_car(sexp) - self.properties: Dict[str, str] = {test_cast(prop[1], str): test_cast(prop[2], str) - for prop in sexp_dict.get('property', [])} - self.refdes = self.properties.get("Reference", "") - self.lib = test_cast(extract_only(sexp_dict['lib_id'])[1], str) - # lib_name (if present) is used for sheet-specific modified symbols to reference that modified symbol - # but is not a user-specified name, so the interface symbol name is still lib_id - if 'lib_name' in sexp_dict: - self.lib_ref = test_cast(extract_only(sexp_dict['lib_name'])[1], str) - else: - self.lib_ref = test_cast(extract_only(sexp_dict['lib_id'])[1], str) - self.pos = parse_at(extract_only(sexp_dict['at'])) - - if 'mirror' in sexp_dict: - self.mirror = parse_symbol(extract_only(sexp_dict['mirror'])[1]) - else: - self.mirror = '' + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.refdes}, {self.lib} @ {self.pos})" + + def __init__(self, sexp: List[Any]): + assert parse_symbol(sexp[0]) == "symbol" + sexp_dict = group_by_car(sexp) + self.properties: Dict[str, str] = { + test_cast(prop[1], str): test_cast(prop[2], str) for prop in sexp_dict.get("property", []) + } + self.refdes = self.properties.get("Reference", "") + self.lib = test_cast(extract_only(sexp_dict["lib_id"])[1], str) + # lib_name (if present) is used for sheet-specific modified symbols to reference that modified symbol + # but is not a user-specified name, so the interface symbol name is still lib_id + if "lib_name" in sexp_dict: + self.lib_ref = test_cast(extract_only(sexp_dict["lib_name"])[1], str) + else: + self.lib_ref = test_cast(extract_only(sexp_dict["lib_id"])[1], str) + self.pos = parse_at(extract_only(sexp_dict["at"])) + + if "mirror" in sexp_dict: + self.mirror = parse_symbol(extract_only(sexp_dict["mirror"])[1]) + else: + self.mirror = "" class KiCadPin: - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.refdes}.{self.pin_number} @ {self.pt})" - - def __init__(self, symbol: KiCadSymbol, pin: KiCadLibPin): - self.pin = pin - self.symbol = symbol - self.refdes = self.symbol.refdes - self.pin_name = self.pin.name - self.pin_number = self.pin.number - - pin_x = pin.pos[0] - pin_y = pin.pos[1] - symbol_rot = math.radians(symbol.pos[2]) # degrees to radians - if symbol.mirror == '': - pass - elif symbol.mirror == 'x': # mirror along x axis - pin_y = -pin_y - symbol_rot = -symbol_rot - elif symbol.mirror == 'y': # mirror along y axis - assert symbol_rot == 0 # KiCad doesn't seem to generate Y-mirror with rotation, so this can't be tested - pin_x = -pin_x - else: - raise ValueError(f"unexpected mirror value {symbol.mirror}") - - self.pt = ( # round so the positions line up exactly - round(symbol.pos[0] + pin_x * math.cos(symbol_rot) - pin_y * math.sin(symbol_rot)), - round(symbol.pos[1] - pin_x * math.sin(symbol_rot) - pin_y * math.cos(symbol_rot)) - ) + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.refdes}.{self.pin_number} @ {self.pt})" + + def __init__(self, symbol: KiCadSymbol, pin: KiCadLibPin): + self.pin = pin + self.symbol = symbol + self.refdes = self.symbol.refdes + self.pin_name = self.pin.name + self.pin_number = self.pin.number + + pin_x = pin.pos[0] + pin_y = pin.pos[1] + symbol_rot = math.radians(symbol.pos[2]) # degrees to radians + if symbol.mirror == "": + pass + elif symbol.mirror == "x": # mirror along x axis + pin_y = -pin_y + symbol_rot = -symbol_rot + elif symbol.mirror == "y": # mirror along y axis + assert symbol_rot == 0 # KiCad doesn't seem to generate Y-mirror with rotation, so this can't be tested + pin_x = -pin_x + else: + raise ValueError(f"unexpected mirror value {symbol.mirror}") + + self.pt = ( # round so the positions line up exactly + round(symbol.pos[0] + pin_x * math.cos(symbol_rot) - pin_y * math.sin(symbol_rot)), + round(symbol.pos[1] - pin_x * math.sin(symbol_rot) - pin_y * math.cos(symbol_rot)), + ) class ParsedNet(NamedTuple): - labels: List[KiCadTunnel] - pins: List[KiCadPin] + labels: List[KiCadTunnel] + pins: List[KiCadPin] - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}(labels={self.labels}, pins={self.pins})" + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}(labels={self.labels}, pins={self.pins})" class SchematicOrder(Enum): - xy = 'xy' # in position order, X then Y (left to right first, then top to bottom) - file = 'file' # in order symbols are defined in the file + xy = "xy" # in position order, X then Y (left to right first, then top to bottom) + file = "file" # in order symbols are defined in the file class KiCadSchematic: - T = TypeVar('T') - @staticmethod - def _connected_components(connected: List[List[T]]) -> List[List[T]]: - """Given a list of connections (as a list components in that connection), - returns the set of connected components by combining connections where components - are connected to each other. - Somewhat maintains order""" - # transform the list of connections into an adjacency list - adjacency: Dict[KiCadSchematic.T, List[KiCadSchematic.T]] = {} - for components in connected: - for component in components: - adjacency.setdefault(component, []).extend(components) - # traverse the graph and build connected components - all_components = list(itertools.chain(*connected)) - seen_components = set() - connected_components: List[List[KiCadSchematic.T]] = [] - for component in all_components: - if component in seen_components: # already seen and part of another connection - continue - connection_components = [] - - def traverse_component(component: KiCadSchematic.T) -> None: - if component in seen_components: # already seen, don't need to traverse again - return - seen_components.add(component) - - for connected in adjacency.get(component, []): - if connected not in connection_components: - connection_components.append(connected) - - for connected in adjacency.get(component, []): - traverse_component(connected) - - traverse_component(component) - connected_components.append(connection_components) - return connected_components - - @staticmethod - def _deduplicate_list(elts: Iterable[T]) -> List[T]: - """Deduplicates a list while preserving order, keeping the first unique element""" - seen: Set[KiCadSchematic.T] = set() - output = [] - for elt in elts: - if elt not in seen: - output.append(elt) - seen.add(elt) - return output - - def __init__(self, data: str, order: SchematicOrder = SchematicOrder.xy): - schematic_top = sexpdata.loads(data) - assert parse_symbol(schematic_top[0]) == 'kicad_sch' - sexp_dict = group_by_car(schematic_top) - - self.lib_symbols = {symbol.name: symbol - for symbol in [KiCadLibSymbol(elt) - for elt in extract_only(sexp_dict.get('lib_symbols', []))[1:]]} # discard car - - wires = [KiCadWire(elt) for elt in sexp_dict.get('wire', [])] - labels: List[KiCadTunnel] = [KiCadLabel(elt) for elt in sexp_dict.get('label', [])] - labels.extend([KiCadGlobalLabel(elt) for elt in sexp_dict.get('global_label', [])]) - labels.extend([KiCadHierarchicalLabel(elt) for elt in sexp_dict.get('hierarchical_label', [])]) - markers: List[KiCadMarker] = [KiCadNoConnect(elt) for elt in sexp_dict.get('no_connect', [])] - # TODO support hierarchy with sheet_instances and symbol_instances - # TODO also check for intersections - currently pins and labels need to be at wire ends - - all_symbols = [KiCadSymbol(elt) for elt in sexp_dict.get('symbol', [])] - # separate out power and non-power symbols, power symbols stay internal - symbols = [symbol for symbol in all_symbols if not self.lib_symbols[symbol.lib_ref].is_power] - power_symbols = [symbol for symbol in all_symbols if self.lib_symbols[symbol.lib_ref].is_power] - - # sorting allows for order-stability which allows for refdes-stability - if order == SchematicOrder.xy: - self.symbols = sorted(symbols, key=lambda elt: elt.pos) - elif order == SchematicOrder.file: - self.symbols = symbols # preserve loaded order - - symbol_pins = list(itertools.chain(*[[KiCadPin(symbol, pin) - for pin in self.lib_symbols[symbol.lib_ref].pins] - for symbol in self.symbols])) - power_pins = list(itertools.chain(*[[KiCadPin(symbol, pin) - for pin in self.lib_symbols[symbol.lib_ref].pins] - for symbol in power_symbols])) - labels.extend([KiCadPowerLabel(power_pin.symbol.properties['Value'], power_pin.pt) - for power_pin in power_pins]) - - # now, actually build the netlist, with graph traversal to find connected components - # and by converting wires (and stuff) into lists of connected points - - # start by building a list of connected wire points - wires_points: List[List[PointType]] = [] - wires_points.extend([pin.pt] for pin in symbol_pins) # make sure all points are represented - wires_points.extend([label.pt] for label in labels) - wires_points.extend([marker.pt] for marker in markers) - wires_points.extend([[wire.pt1, wire.pt2] for wire in wires]) - wires_points = self._connected_components(wires_points) - - # build a list of pins and labels by point - elts_by_point: Dict[PointType, List[Union[KiCadPin, KiCadTunnel, KiCadMarker]]] = {} - labels_by_name: Dict[str, List[Union[KiCadPin, KiCadTunnel, KiCadMarker]]] = {} # it will not be a KiCadPin but needed for invariant typing - for pin in symbol_pins: - elts_by_point.setdefault(pin.pt, []).append(pin) - for label in labels: - elts_by_point.setdefault(label.pt, []).append(label) - labels_by_name.setdefault(label.name, []).append(label) - for marker in markers: - elts_by_point.setdefault(marker.pt, []).append(marker) - - # transform that into a list of elements - # make sure all elements are seeded in the list - nets_elts: List[List[Union[KiCadPin, KiCadTunnel, KiCadMarker]]] = [ # transform points into KiCad elements - self._deduplicate_list(itertools.chain(*[elts_by_point.get(point, []) - for point in points])) - for points in wires_points] - - # sanity check to ensure there aren't any degenerate connections - # labels don't create wire endpoints / junctions and we don't process connections mid-wire - for net_elts in nets_elts: - if len(net_elts) == 1 and isinstance(net_elts[0], KiCadBaseLabel): - raise ValueError(f"disconnected net label at {net_elts[0].name}") - - # run connections again, to propagate label connections - nets_elts.extend([name_labels for (name, name_labels) in labels_by_name.items()]) - nets_elts = self._connected_components(nets_elts) - - self.nets: List[ParsedNet] = [] - for net_elts in nets_elts: - net_pins = [elt for elt in net_elts if isinstance(elt, KiCadPin)] - net_labels = [elt for elt in net_elts if isinstance(elt, KiCadTunnel)] - parsed_net = ParsedNet(net_labels, net_pins) - - net_markers = [elt for elt in net_elts if isinstance(elt, KiCadMarker)] - for marker in net_markers: - if isinstance(marker, KiCadNoConnect): - if len(net_pins) < 1: - raise ValueError(f"no-connect with no connected pins at {parsed_net}") - elif len(net_pins) > 1: - raise ValueError(f"no-connect with multiple connected pins at {parsed_net}") - - self.nets.append(parsed_net) + T = TypeVar("T") + + @staticmethod + def _connected_components(connected: List[List[T]]) -> List[List[T]]: + """Given a list of connections (as a list components in that connection), + returns the set of connected components by combining connections where components + are connected to each other. + Somewhat maintains order""" + # transform the list of connections into an adjacency list + adjacency: Dict[KiCadSchematic.T, List[KiCadSchematic.T]] = {} + for components in connected: + for component in components: + adjacency.setdefault(component, []).extend(components) + # traverse the graph and build connected components + all_components = list(itertools.chain(*connected)) + seen_components = set() + connected_components: List[List[KiCadSchematic.T]] = [] + for component in all_components: + if component in seen_components: # already seen and part of another connection + continue + connection_components = [] + + def traverse_component(component: KiCadSchematic.T) -> None: + if component in seen_components: # already seen, don't need to traverse again + return + seen_components.add(component) + + for connected in adjacency.get(component, []): + if connected not in connection_components: + connection_components.append(connected) + + for connected in adjacency.get(component, []): + traverse_component(connected) + + traverse_component(component) + connected_components.append(connection_components) + return connected_components + + @staticmethod + def _deduplicate_list(elts: Iterable[T]) -> List[T]: + """Deduplicates a list while preserving order, keeping the first unique element""" + seen: Set[KiCadSchematic.T] = set() + output = [] + for elt in elts: + if elt not in seen: + output.append(elt) + seen.add(elt) + return output + + def __init__(self, data: str, order: SchematicOrder = SchematicOrder.xy): + schematic_top = sexpdata.loads(data) + assert parse_symbol(schematic_top[0]) == "kicad_sch" + sexp_dict = group_by_car(schematic_top) + + self.lib_symbols = { + symbol.name: symbol + for symbol in [KiCadLibSymbol(elt) for elt in extract_only(sexp_dict.get("lib_symbols", []))[1:]] + } # discard car + + wires = [KiCadWire(elt) for elt in sexp_dict.get("wire", [])] + labels: List[KiCadTunnel] = [KiCadLabel(elt) for elt in sexp_dict.get("label", [])] + labels.extend([KiCadGlobalLabel(elt) for elt in sexp_dict.get("global_label", [])]) + labels.extend([KiCadHierarchicalLabel(elt) for elt in sexp_dict.get("hierarchical_label", [])]) + markers: List[KiCadMarker] = [KiCadNoConnect(elt) for elt in sexp_dict.get("no_connect", [])] + # TODO support hierarchy with sheet_instances and symbol_instances + # TODO also check for intersections - currently pins and labels need to be at wire ends + + all_symbols = [KiCadSymbol(elt) for elt in sexp_dict.get("symbol", [])] + # separate out power and non-power symbols, power symbols stay internal + symbols = [symbol for symbol in all_symbols if not self.lib_symbols[symbol.lib_ref].is_power] + power_symbols = [symbol for symbol in all_symbols if self.lib_symbols[symbol.lib_ref].is_power] + + # sorting allows for order-stability which allows for refdes-stability + if order == SchematicOrder.xy: + self.symbols = sorted(symbols, key=lambda elt: elt.pos) + elif order == SchematicOrder.file: + self.symbols = symbols # preserve loaded order + + symbol_pins = list( + itertools.chain( + *[[KiCadPin(symbol, pin) for pin in self.lib_symbols[symbol.lib_ref].pins] for symbol in self.symbols] + ) + ) + power_pins = list( + itertools.chain( + *[[KiCadPin(symbol, pin) for pin in self.lib_symbols[symbol.lib_ref].pins] for symbol in power_symbols] + ) + ) + labels.extend([KiCadPowerLabel(power_pin.symbol.properties["Value"], power_pin.pt) for power_pin in power_pins]) + + # now, actually build the netlist, with graph traversal to find connected components + # and by converting wires (and stuff) into lists of connected points + + # start by building a list of connected wire points + wires_points: List[List[PointType]] = [] + wires_points.extend([pin.pt] for pin in symbol_pins) # make sure all points are represented + wires_points.extend([label.pt] for label in labels) + wires_points.extend([marker.pt] for marker in markers) + wires_points.extend([[wire.pt1, wire.pt2] for wire in wires]) + wires_points = self._connected_components(wires_points) + + # build a list of pins and labels by point + elts_by_point: Dict[PointType, List[Union[KiCadPin, KiCadTunnel, KiCadMarker]]] = {} + labels_by_name: Dict[str, List[Union[KiCadPin, KiCadTunnel, KiCadMarker]]] = ( + {} + ) # it will not be a KiCadPin but needed for invariant typing + for pin in symbol_pins: + elts_by_point.setdefault(pin.pt, []).append(pin) + for label in labels: + elts_by_point.setdefault(label.pt, []).append(label) + labels_by_name.setdefault(label.name, []).append(label) + for marker in markers: + elts_by_point.setdefault(marker.pt, []).append(marker) + + # transform that into a list of elements + # make sure all elements are seeded in the list + nets_elts: List[List[Union[KiCadPin, KiCadTunnel, KiCadMarker]]] = [ # transform points into KiCad elements + self._deduplicate_list(itertools.chain(*[elts_by_point.get(point, []) for point in points])) + for points in wires_points + ] + + # sanity check to ensure there aren't any degenerate connections + # labels don't create wire endpoints / junctions and we don't process connections mid-wire + for net_elts in nets_elts: + if len(net_elts) == 1 and isinstance(net_elts[0], KiCadBaseLabel): + raise ValueError(f"disconnected net label at {net_elts[0].name}") + + # run connections again, to propagate label connections + nets_elts.extend([name_labels for (name, name_labels) in labels_by_name.items()]) + nets_elts = self._connected_components(nets_elts) + + self.nets: List[ParsedNet] = [] + for net_elts in nets_elts: + net_pins = [elt for elt in net_elts if isinstance(elt, KiCadPin)] + net_labels = [elt for elt in net_elts if isinstance(elt, KiCadTunnel)] + parsed_net = ParsedNet(net_labels, net_pins) + + net_markers = [elt for elt in net_elts if isinstance(elt, KiCadMarker)] + for marker in net_markers: + if isinstance(marker, KiCadNoConnect): + if len(net_pins) < 1: + raise ValueError(f"no-connect with no connected pins at {parsed_net}") + elif len(net_pins) > 1: + raise ValueError(f"no-connect with multiple connected pins at {parsed_net}") + + self.nets.append(parsed_net) diff --git a/edg/electronics_model/KicadFootprintData.py b/edg/electronics_model/KicadFootprintData.py index 8aafa5c51..a6fce0d77 100644 --- a/edg/electronics_model/KicadFootprintData.py +++ b/edg/electronics_model/KicadFootprintData.py @@ -5,38 +5,38 @@ class FootprintData(BaseModel): - area: float - bbox: Tuple[float, float, float, float] # [x_min, y_min, x_max, y_max] + area: float + bbox: Tuple[float, float, float, float] # [x_min, y_min, x_max, y_max] class FootprintJson(RootModel[Dict[str, FootprintData]]): # script relpath imports are weird so this is duplicated here - root: dict[str, FootprintData] # footprint name -> data + root: dict[str, FootprintData] # footprint name -> data class FootprintDataTable: - _table: Optional[FootprintJson] = None - - @classmethod - def _get_table(cls) -> FootprintJson: - if cls._table is None: - with open(os.path.join(os.path.dirname(__file__), "resources", "kicad_footprints.json"), 'r') as f: - cls._table = FootprintJson.model_validate_json(f.read()) - return cls._table - - @classmethod - def area_of(cls, footprint: str) -> float: - """Returns the area of a footprint, returning infinity if unavailable""" - elt = cls._get_table().root.get(footprint) - if elt is None: - return float('inf') - else: - return elt.area - - @classmethod - def bbox_of(cls, footprint: str) -> Optional[Tuple[float, float, float, float]]: - """Returns the bounding box of a footprint, returning None if unavailable""" - elt = cls._get_table().root.get(footprint) - if elt is None: - return None - else: - return elt.bbox + _table: Optional[FootprintJson] = None + + @classmethod + def _get_table(cls) -> FootprintJson: + if cls._table is None: + with open(os.path.join(os.path.dirname(__file__), "resources", "kicad_footprints.json"), "r") as f: + cls._table = FootprintJson.model_validate_json(f.read()) + return cls._table + + @classmethod + def area_of(cls, footprint: str) -> float: + """Returns the area of a footprint, returning infinity if unavailable""" + elt = cls._get_table().root.get(footprint) + if elt is None: + return float("inf") + else: + return elt.area + + @classmethod + def bbox_of(cls, footprint: str) -> Optional[Tuple[float, float, float, float]]: + """Returns the bounding box of a footprint, returning None if unavailable""" + elt = cls._get_table().root.get(footprint) + if elt is None: + return None + else: + return elt.bbox diff --git a/edg/electronics_model/NetlistBackend.py b/edg/electronics_model/NetlistBackend.py index bc4c7843c..2d205c64c 100644 --- a/edg/electronics_model/NetlistBackend.py +++ b/edg/electronics_model/NetlistBackend.py @@ -9,23 +9,21 @@ class NetlistBackend(BaseBackend): - @override - def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[edgir.LocalPath, str]]: - if set(args.keys()) - {'RefdesMode'} != set(): - raise ValueError("Invalid argument found in args") - refdes_mode_arg = args.get("RefdesMode", "refdesPathNameValue") - if refdes_mode_arg == 'pathName': - refdes_mode = kicad.RefdesMode.Pathname - elif refdes_mode_arg == 'refdes': - refdes_mode = kicad.RefdesMode.Conventional - elif refdes_mode_arg == 'refdesPathNameValue': - refdes_mode = kicad.RefdesMode.PathnameAsValue - else: - raise ValueError(f"Invalid RefdesMode value {refdes_mode_arg}") + @override + def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[edgir.LocalPath, str]]: + if set(args.keys()) - {"RefdesMode"} != set(): + raise ValueError("Invalid argument found in args") + refdes_mode_arg = args.get("RefdesMode", "refdesPathNameValue") + if refdes_mode_arg == "pathName": + refdes_mode = kicad.RefdesMode.Pathname + elif refdes_mode_arg == "refdes": + refdes_mode = kicad.RefdesMode.Conventional + elif refdes_mode_arg == "refdesPathNameValue": + refdes_mode = kicad.RefdesMode.PathnameAsValue + else: + raise ValueError(f"Invalid RefdesMode value {refdes_mode_arg}") - netlist = NetlistTransform(design).run() - netlist_string = kicad.generate_netlist(netlist, refdes_mode) + netlist = NetlistTransform(design).run() + netlist_string = kicad.generate_netlist(netlist, refdes_mode) - return [ - (edgir.LocalPath(), netlist_string) - ] + return [(edgir.LocalPath(), netlist_string)] diff --git a/edg/electronics_model/NetlistGenerator.py b/edg/electronics_model/NetlistGenerator.py index a62a357eb..6d6375f75 100644 --- a/edg/electronics_model/NetlistGenerator.py +++ b/edg/electronics_model/NetlistGenerator.py @@ -9,34 +9,37 @@ class InvalidNetlistBlockException(BaseException): - pass + pass class InvalidPackingException(BaseException): - pass + pass class NetBlock(NamedTuple): - footprint: str - refdes: str - part: str - value: str # gets written directly to footprint - full_path: TransformUtil.Path # full path to this footprint - path: List[str] # short path to this footprint - class_path: List[edgir.LibraryPath] # classes on short path to this footprint + footprint: str + refdes: str + part: str + value: str # gets written directly to footprint + full_path: TransformUtil.Path # full path to this footprint + path: List[str] # short path to this footprint + class_path: List[edgir.LibraryPath] # classes on short path to this footprint + class NetPin(NamedTuple): - block_path: TransformUtil.Path # full path to the block - pin_name: str + block_path: TransformUtil.Path # full path to the block + pin_name: str + class Net(NamedTuple): - name: str - pins: List[NetPin] - ports: List[TransformUtil.Path] + name: str + pins: List[NetPin] + ports: List[TransformUtil.Path] + class Netlist(NamedTuple): - blocks: List[NetBlock] - nets: List[Net] + blocks: List[NetBlock] + nets: List[Net] # The concept of board scopes is footprints associated with a scope (defined as a path) that is a board, @@ -44,303 +47,326 @@ class Netlist(NamedTuple): # The default (top-level) board has TransformUtil.Path.empty(). # None board scope means the footprints do not exist (virtual component), eg the associated blocks were for modeling. class BoardScope(NamedTuple): - path: TransformUtil.Path # root path - footprints: Dict[TransformUtil.Path, NetBlock] # Path -> Block footprint - edges: Dict[TransformUtil.Path, List[TransformUtil.Path]] # Port Path -> connected Port Paths - pins: Dict[TransformUtil.Path, List[NetPin]] # mapping from Port to pad - assert_connected: List[Tuple[TransformUtil.Path, TransformUtil.Path]] + path: TransformUtil.Path # root path + footprints: Dict[TransformUtil.Path, NetBlock] # Path -> Block footprint + edges: Dict[TransformUtil.Path, List[TransformUtil.Path]] # Port Path -> connected Port Paths + pins: Dict[TransformUtil.Path, List[NetPin]] # mapping from Port to pad + assert_connected: List[Tuple[TransformUtil.Path, TransformUtil.Path]] - @classmethod - def empty(cls, path: TransformUtil.Path) -> 'BoardScope': # returns a fresh, empty BordScope - return BoardScope(path, {}, {}, {}, []) + @classmethod + def empty(cls, path: TransformUtil.Path) -> "BoardScope": # returns a fresh, empty BordScope + return BoardScope(path, {}, {}, {}, []) Scopes = Dict[TransformUtil.Path, Optional[BoardScope]] # Block -> board scope (reference, aliased across entries) -ClassPaths = Dict[TransformUtil.Path, List[edgir.LibraryPath]] # Path -> class names corresponding to shortened path name +ClassPaths = Dict[ + TransformUtil.Path, List[edgir.LibraryPath] +] # Path -> class names corresponding to shortened path name + + class NetlistTransform(TransformUtil.Transform): - @staticmethod - def flatten_port(path: TransformUtil.Path, port: edgir.PortLike) -> Iterable[TransformUtil.Path]: - if port.HasField('port'): - return [path] - elif port.HasField('array') and port.array.HasField('ports'): - return chain(*[NetlistTransform.flatten_port(path.append_port(port_pair.name), port_pair.value) - for port_pair in port.array.ports.ports]) - else: - raise ValueError(f"don't know how to flatten netlistable port {port}") - - def __init__(self, design: CompiledDesign): - self.all_scopes = [BoardScope.empty(TransformUtil.Path.empty())] # list of unique scopes - self.scopes: Scopes = {TransformUtil.Path.empty(): self.all_scopes[0]} - - self.short_paths: Dict[TransformUtil.Path, List[str]] = {TransformUtil.Path.empty(): []} # seed root - self.class_paths: ClassPaths = {TransformUtil.Path.empty(): []} # seed root - - self.design = design - - def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, edgir.LinkArray, edgir.HierarchyBlock]) -> None: - # TODO may need rethought to support multi-board assemblies - scope = self.scopes[path] # including footprint and exports, and everything within a link - internal_scope = scope # for internal blocks - - if isinstance(block, edgir.HierarchyBlock): - if 'fp_is_wrapper' in block.meta.members.node: # wrapper internal blocks ignored - internal_scope = None - for block_pair in block.blocks: - self.scopes[path.append_block(block_pair.name)] = internal_scope - for link_pair in block.links: # links considered to be the same scope as self - self.scopes[path.append_link(link_pair.name)] = scope - - # generate short paths for children first, for Blocks only - main_internal_blocks: Dict[str, edgir.BlockLike] = {} - other_internal_blocks: Dict[str, edgir.BlockLike] = {} - - for block_pair in block.blocks: - subblock = block_pair.value - # ignore pseudoblocks like bridges and adapters that have no internals - if not subblock.hierarchy.blocks and 'fp_is_footprint' not in subblock.hierarchy.meta.members.node: - other_internal_blocks[block_pair.name] = block_pair.value + @staticmethod + def flatten_port(path: TransformUtil.Path, port: edgir.PortLike) -> Iterable[TransformUtil.Path]: + if port.HasField("port"): + return [path] + elif port.HasField("array") and port.array.HasField("ports"): + return chain( + *[ + NetlistTransform.flatten_port(path.append_port(port_pair.name), port_pair.value) + for port_pair in port.array.ports.ports + ] + ) else: - main_internal_blocks[block_pair.name] = block_pair.value - - short_path = self.short_paths[path] - class_path = self.class_paths[path] - - if len(main_internal_blocks) == 1 and short_path: # never shorten top-level blocks - name = list(main_internal_blocks.keys())[0] - self.short_paths[path.append_block(name)] = short_path - self.class_paths[path.append_block(name)] = class_path - else: - for (name, subblock) in main_internal_blocks.items(): - self.short_paths[path.append_block(name)] = short_path + [name] - self.class_paths[path.append_block(name)] = class_path + [subblock.hierarchy.self_class] - - for (name, subblock) in other_internal_blocks.items(): - self.short_paths[path.append_block(name)] = short_path + [name] - self.class_paths[path.append_block(name)] = class_path + [subblock.hierarchy.self_class] - elif isinstance(block, (edgir.Link, edgir.LinkArray)): - for link_pair in block.links: - self.scopes[path.append_link(link_pair.name)] = scope - - if 'nets' in block.meta.members.node and scope is not None: - # add self as a net - # list conversion to deal with iterable-once - flat_ports = list(chain(*[self.flatten_port(path.append_port(port_pair.name), port_pair.value) - for port_pair in block.ports])) - scope.edges.setdefault(path, []).extend(flat_ports) - for port_path in flat_ports: - scope.edges.setdefault(port_path, []).append(path) - - if 'nets_packed' in block.meta.members.node and scope is not None: - # this connects the first source to all destinations, then asserts all the sources are equal - # this leaves the sources unconnected, to be connected externally and checked at the end - src_port_name = block.meta.members.node['nets_packed'].members.node['src'].text_leaf - dst_port_name = block.meta.members.node['nets_packed'].members.node['dst'].text_leaf - flat_srcs = list(self.flatten_port(path.append_port(src_port_name), edgir.pair_get(block.ports, src_port_name))) - flat_dsts = list(self.flatten_port(path.append_port(dst_port_name), edgir.pair_get(block.ports, dst_port_name))) - assert flat_srcs, "missing source port(s) for packed net" - for dst_path in flat_dsts: - scope.edges.setdefault(flat_srcs[0], []).append(dst_path) - scope.edges.setdefault(dst_path, []).append(flat_srcs[0]) - for src_path in flat_srcs: # assert all sources connected - for dst_path in flat_srcs: - scope.assert_connected.append((src_path, dst_path)) - - if 'fp_is_footprint' in block.meta.members.node and scope is not None: - footprint_name = self.design.get_value(path.to_tuple() + ('fp_footprint',)) - footprint_pinning = self.design.get_value(path.to_tuple() + ('fp_pinning',)) - mfr = self.design.get_value(path.to_tuple() + ('fp_mfr',)) - part = self.design.get_value(path.to_tuple() + ('fp_part',)) - value = self.design.get_value(path.to_tuple() + ('fp_value',)) - refdes = self.design.get_value(path.to_tuple() + ('fp_refdes',)) - lcsc_part = self.design.get_value(path.to_tuple() + ('lcsc_part',)) - - assert isinstance(footprint_name, str) - assert isinstance(footprint_pinning, list) - assert isinstance(mfr, str) or mfr is None - assert isinstance(part, str) or part is None - assert isinstance(value, str) or value is None - assert isinstance(lcsc_part, str) or lcsc_part is None - assert isinstance(refdes, str) - - part_comps = [ - part, - f"({mfr})" if mfr else "" - ] - part_str = " ".join(filter(None, part_comps)) - value_str = value if value else (part if part else '') - scope.footprints[path] = NetBlock( - footprint_name, - refdes, - part_str, - value_str, - path, - self.short_paths[path], - self.class_paths[path] - ) - - for pin_spec in footprint_pinning: - assert isinstance(pin_spec, str) - pin_spec_split = pin_spec.split('=') - assert len(pin_spec_split) == 2 - pin_name = pin_spec_split[0] - pin_port_path = edgir.LocalPathList(pin_spec_split[1].split('.')) - - src_path = path.follow(pin_port_path, block)[0] - scope.edges.setdefault(src_path, []) # make sure there is a port entry so single-pin nets are named - scope.pins.setdefault(src_path, []).append(NetPin(path, pin_name)) - - for constraint_pair in block.constraints: - if scope is not None: - if constraint_pair.value.HasField('connected'): - self.process_connected(path, block, scope, constraint_pair.value.connected) - elif constraint_pair.value.HasField('connectedArray'): - for expanded_connect in constraint_pair.value.connectedArray.expanded: - self.process_connected(path, block, scope, expanded_connect) - elif constraint_pair.value.HasField('exported'): - self.process_exported(path, block, scope, constraint_pair.value.exported) - elif constraint_pair.value.HasField('exportedArray'): - for expanded_export in constraint_pair.value.exportedArray.expanded: - self.process_exported(path, block, scope, expanded_export) - elif constraint_pair.value.HasField('exportedTunnel'): - self.process_exported(path, block, scope, constraint_pair.value.exportedTunnel) - - def process_connected(self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope, - constraint: edgir.ConnectedExpr) -> None: - if constraint.expanded: - assert len(constraint.expanded) == 1 - self.process_connected(path, current, scope, constraint.expanded[0]) - return - assert constraint.block_port.HasField('ref') - assert constraint.link_port.HasField('ref') - self.connect_ports( - scope, - path.follow(constraint.block_port.ref, current), - path.follow(constraint.link_port.ref, current)) - - def process_exported(self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope, - constraint: edgir.ExportedExpr) -> None: - if constraint.expanded: - assert len(constraint.expanded) == 1 - self.process_exported(path, current, scope, constraint.expanded[0]) - return - assert constraint.internal_block_port.HasField('ref') - assert constraint.exterior_port.HasField('ref') - self.connect_ports( - scope, - path.follow(constraint.internal_block_port.ref, current), - path.follow(constraint.exterior_port.ref, current)) - - def connect_ports(self, scope: BoardScope, elt1: Tuple[TransformUtil.Path, edgir.EltTypes], - elt2: Tuple[TransformUtil.Path, edgir.EltTypes]) -> None: - """Recursively connect ports as applicable""" - if isinstance(elt1[1], edgir.Port) and isinstance(elt2[1], edgir.Port): - scope.edges.setdefault(elt1[0], []).append(elt2[0]) - scope.edges.setdefault(elt2[0], []).append(elt1[0]) - elif isinstance(elt1[1], edgir.Bundle) and isinstance(elt2[1], edgir.Bundle): - elt1_names = list(map(lambda pair: pair.name, elt1[1].ports)) - elt2_names = list(map(lambda pair: pair.name, elt2[1].ports)) - assert elt1_names == elt2_names, f"mismatched bundle types {elt1}, {elt2}" - for key in elt2_names: + raise ValueError(f"don't know how to flatten netlistable port {port}") + + def __init__(self, design: CompiledDesign): + self.all_scopes = [BoardScope.empty(TransformUtil.Path.empty())] # list of unique scopes + self.scopes: Scopes = {TransformUtil.Path.empty(): self.all_scopes[0]} + + self.short_paths: Dict[TransformUtil.Path, List[str]] = {TransformUtil.Path.empty(): []} # seed root + self.class_paths: ClassPaths = {TransformUtil.Path.empty(): []} # seed root + + self.design = design + + def process_blocklike( + self, path: TransformUtil.Path, block: Union[edgir.Link, edgir.LinkArray, edgir.HierarchyBlock] + ) -> None: + # TODO may need rethought to support multi-board assemblies + scope = self.scopes[path] # including footprint and exports, and everything within a link + internal_scope = scope # for internal blocks + + if isinstance(block, edgir.HierarchyBlock): + if "fp_is_wrapper" in block.meta.members.node: # wrapper internal blocks ignored + internal_scope = None + for block_pair in block.blocks: + self.scopes[path.append_block(block_pair.name)] = internal_scope + for link_pair in block.links: # links considered to be the same scope as self + self.scopes[path.append_link(link_pair.name)] = scope + + # generate short paths for children first, for Blocks only + main_internal_blocks: Dict[str, edgir.BlockLike] = {} + other_internal_blocks: Dict[str, edgir.BlockLike] = {} + + for block_pair in block.blocks: + subblock = block_pair.value + # ignore pseudoblocks like bridges and adapters that have no internals + if not subblock.hierarchy.blocks and "fp_is_footprint" not in subblock.hierarchy.meta.members.node: + other_internal_blocks[block_pair.name] = block_pair.value + else: + main_internal_blocks[block_pair.name] = block_pair.value + + short_path = self.short_paths[path] + class_path = self.class_paths[path] + + if len(main_internal_blocks) == 1 and short_path: # never shorten top-level blocks + name = list(main_internal_blocks.keys())[0] + self.short_paths[path.append_block(name)] = short_path + self.class_paths[path.append_block(name)] = class_path + else: + for name, subblock in main_internal_blocks.items(): + self.short_paths[path.append_block(name)] = short_path + [name] + self.class_paths[path.append_block(name)] = class_path + [subblock.hierarchy.self_class] + + for name, subblock in other_internal_blocks.items(): + self.short_paths[path.append_block(name)] = short_path + [name] + self.class_paths[path.append_block(name)] = class_path + [subblock.hierarchy.self_class] + elif isinstance(block, (edgir.Link, edgir.LinkArray)): + for link_pair in block.links: + self.scopes[path.append_link(link_pair.name)] = scope + + if "nets" in block.meta.members.node and scope is not None: + # add self as a net + # list conversion to deal with iterable-once + flat_ports = list( + chain( + *[self.flatten_port(path.append_port(port_pair.name), port_pair.value) for port_pair in block.ports] + ) + ) + scope.edges.setdefault(path, []).extend(flat_ports) + for port_path in flat_ports: + scope.edges.setdefault(port_path, []).append(path) + + if "nets_packed" in block.meta.members.node and scope is not None: + # this connects the first source to all destinations, then asserts all the sources are equal + # this leaves the sources unconnected, to be connected externally and checked at the end + src_port_name = block.meta.members.node["nets_packed"].members.node["src"].text_leaf + dst_port_name = block.meta.members.node["nets_packed"].members.node["dst"].text_leaf + flat_srcs = list( + self.flatten_port(path.append_port(src_port_name), edgir.pair_get(block.ports, src_port_name)) + ) + flat_dsts = list( + self.flatten_port(path.append_port(dst_port_name), edgir.pair_get(block.ports, dst_port_name)) + ) + assert flat_srcs, "missing source port(s) for packed net" + for dst_path in flat_dsts: + scope.edges.setdefault(flat_srcs[0], []).append(dst_path) + scope.edges.setdefault(dst_path, []).append(flat_srcs[0]) + for src_path in flat_srcs: # assert all sources connected + for dst_path in flat_srcs: + scope.assert_connected.append((src_path, dst_path)) + + if "fp_is_footprint" in block.meta.members.node and scope is not None: + footprint_name = self.design.get_value(path.to_tuple() + ("fp_footprint",)) + footprint_pinning = self.design.get_value(path.to_tuple() + ("fp_pinning",)) + mfr = self.design.get_value(path.to_tuple() + ("fp_mfr",)) + part = self.design.get_value(path.to_tuple() + ("fp_part",)) + value = self.design.get_value(path.to_tuple() + ("fp_value",)) + refdes = self.design.get_value(path.to_tuple() + ("fp_refdes",)) + lcsc_part = self.design.get_value(path.to_tuple() + ("lcsc_part",)) + + assert isinstance(footprint_name, str) + assert isinstance(footprint_pinning, list) + assert isinstance(mfr, str) or mfr is None + assert isinstance(part, str) or part is None + assert isinstance(value, str) or value is None + assert isinstance(lcsc_part, str) or lcsc_part is None + assert isinstance(refdes, str) + + part_comps = [part, f"({mfr})" if mfr else ""] + part_str = " ".join(filter(None, part_comps)) + value_str = value if value else (part if part else "") + scope.footprints[path] = NetBlock( + footprint_name, refdes, part_str, value_str, path, self.short_paths[path], self.class_paths[path] + ) + + for pin_spec in footprint_pinning: + assert isinstance(pin_spec, str) + pin_spec_split = pin_spec.split("=") + assert len(pin_spec_split) == 2 + pin_name = pin_spec_split[0] + pin_port_path = edgir.LocalPathList(pin_spec_split[1].split(".")) + + src_path = path.follow(pin_port_path, block)[0] + scope.edges.setdefault(src_path, []) # make sure there is a port entry so single-pin nets are named + scope.pins.setdefault(src_path, []).append(NetPin(path, pin_name)) + + for constraint_pair in block.constraints: + if scope is not None: + if constraint_pair.value.HasField("connected"): + self.process_connected(path, block, scope, constraint_pair.value.connected) + elif constraint_pair.value.HasField("connectedArray"): + for expanded_connect in constraint_pair.value.connectedArray.expanded: + self.process_connected(path, block, scope, expanded_connect) + elif constraint_pair.value.HasField("exported"): + self.process_exported(path, block, scope, constraint_pair.value.exported) + elif constraint_pair.value.HasField("exportedArray"): + for expanded_export in constraint_pair.value.exportedArray.expanded: + self.process_exported(path, block, scope, expanded_export) + elif constraint_pair.value.HasField("exportedTunnel"): + self.process_exported(path, block, scope, constraint_pair.value.exportedTunnel) + + def process_connected( + self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope, constraint: edgir.ConnectedExpr + ) -> None: + if constraint.expanded: + assert len(constraint.expanded) == 1 + self.process_connected(path, current, scope, constraint.expanded[0]) + return + assert constraint.block_port.HasField("ref") + assert constraint.link_port.HasField("ref") + self.connect_ports( + scope, path.follow(constraint.block_port.ref, current), path.follow(constraint.link_port.ref, current) + ) + + def process_exported( + self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope, constraint: edgir.ExportedExpr + ) -> None: + if constraint.expanded: + assert len(constraint.expanded) == 1 + self.process_exported(path, current, scope, constraint.expanded[0]) + return + assert constraint.internal_block_port.HasField("ref") + assert constraint.exterior_port.HasField("ref") self.connect_ports( - scope, - (elt1[0].append_port(key), edgir.resolve_portlike(edgir.pair_get(elt1[1].ports, key))), - (elt2[0].append_port(key), edgir.resolve_portlike(edgir.pair_get(elt2[1].ports, key)))) - # don't need to create the bundle connect, since Bundles can't be CircuitPorts - else: - raise ValueError(f"can't connect types {elt1}, {elt2}") - - @override - def visit_block(self, context: TransformUtil.TransformContext, block: edgir.BlockTypes) -> None: - self.process_blocklike(context.path, block) - - @override - def visit_link(self, context: TransformUtil.TransformContext, link: edgir.Link) -> None: - self.process_blocklike(context.path, link) - - @override - def visit_linkarray(self, context: TransformUtil.TransformContext, link: edgir.LinkArray) -> None: - self.process_blocklike(context.path, link) - - @staticmethod - def name_net(net: Iterable[TransformUtil.Path], net_prefix: str) -> str: - """Names a net based on all the paths of ports and links that are part of the net.""" - # higher criteria are preferred, True or larger number is preferred - CRITERIA: List[Callable[[TransformUtil.Path], Union[bool, int]]] = [ - lambda pin: not (pin.blocks and pin.blocks[-1].startswith('(adapter)')), - lambda pin: not (pin.links and (pin.links[0].startswith('anon') or pin.links[0].startswith('_'))), - lambda pin: -len(pin.blocks), # prefer shorter block paths - lambda pin: len(pin.links), # prefer longer link paths - lambda pin: -len(pin.ports), # prefer shorter (or no) port lengths - lambda pin: not(pin.ports and pin.ports[-1].isnumeric()), # disprefer number-only ports - ] - def pin_name_goodness(pin1: TransformUtil.Path, pin2: TransformUtil.Path) -> int: - assert not pin1.params and not pin2.params - for test in CRITERIA: - pin1_result = test(pin1) - pin2_result = test(pin2) - if pin1_result == pin2_result: - continue - if isinstance(pin1_result, bool) and isinstance(pin2_result, bool): - if pin1_result: - return -1 - else: - return 1 - elif isinstance(pin1_result, int) and isinstance(pin2_result, int): - return pin2_result - pin1_result + scope, + path.follow(constraint.internal_block_port.ref, current), + path.follow(constraint.exterior_port.ref, current), + ) + + def connect_ports( + self, + scope: BoardScope, + elt1: Tuple[TransformUtil.Path, edgir.EltTypes], + elt2: Tuple[TransformUtil.Path, edgir.EltTypes], + ) -> None: + """Recursively connect ports as applicable""" + if isinstance(elt1[1], edgir.Port) and isinstance(elt2[1], edgir.Port): + scope.edges.setdefault(elt1[0], []).append(elt2[0]) + scope.edges.setdefault(elt2[0], []).append(elt1[0]) + elif isinstance(elt1[1], edgir.Bundle) and isinstance(elt2[1], edgir.Bundle): + elt1_names = list(map(lambda pair: pair.name, elt1[1].ports)) + elt2_names = list(map(lambda pair: pair.name, elt2[1].ports)) + assert elt1_names == elt2_names, f"mismatched bundle types {elt1}, {elt2}" + for key in elt2_names: + self.connect_ports( + scope, + (elt1[0].append_port(key), edgir.resolve_portlike(edgir.pair_get(elt1[1].ports, key))), + (elt2[0].append_port(key), edgir.resolve_portlike(edgir.pair_get(elt2[1].ports, key))), + ) + # don't need to create the bundle connect, since Bundles can't be CircuitPorts + else: + raise ValueError(f"can't connect types {elt1}, {elt2}") + + @override + def visit_block(self, context: TransformUtil.TransformContext, block: edgir.BlockTypes) -> None: + self.process_blocklike(context.path, block) + + @override + def visit_link(self, context: TransformUtil.TransformContext, link: edgir.Link) -> None: + self.process_blocklike(context.path, link) + + @override + def visit_linkarray(self, context: TransformUtil.TransformContext, link: edgir.LinkArray) -> None: + self.process_blocklike(context.path, link) + + @staticmethod + def name_net(net: Iterable[TransformUtil.Path], net_prefix: str) -> str: + """Names a net based on all the paths of ports and links that are part of the net.""" + # higher criteria are preferred, True or larger number is preferred + CRITERIA: List[Callable[[TransformUtil.Path], Union[bool, int]]] = [ + lambda pin: not (pin.blocks and pin.blocks[-1].startswith("(adapter)")), + lambda pin: not (pin.links and (pin.links[0].startswith("anon") or pin.links[0].startswith("_"))), + lambda pin: -len(pin.blocks), # prefer shorter block paths + lambda pin: len(pin.links), # prefer longer link paths + lambda pin: -len(pin.ports), # prefer shorter (or no) port lengths + lambda pin: not (pin.ports and pin.ports[-1].isnumeric()), # disprefer number-only ports + ] + + def pin_name_goodness(pin1: TransformUtil.Path, pin2: TransformUtil.Path) -> int: + assert not pin1.params and not pin2.params + for test in CRITERIA: + pin1_result = test(pin1) + pin2_result = test(pin2) + if pin1_result == pin2_result: + continue + if isinstance(pin1_result, bool) and isinstance(pin2_result, bool): + if pin1_result: + return -1 + else: + return 1 + elif isinstance(pin1_result, int) and isinstance(pin2_result, int): + return pin2_result - pin1_result + else: + raise ValueError("mismatched result types") + return 0 + + best_path = sorted(net, key=cmp_to_key(pin_name_goodness))[0] + + return net_prefix + str(best_path) + + def scope_to_netlist(self, scope: BoardScope) -> Netlist: + # Convert to the netlist format + seen: Set[TransformUtil.Path] = set() + nets: List[List[TransformUtil.Path]] = [] # lists preserve ordering + + for port, conns in scope.edges.items(): + if port not in seen: + curr_net: List[TransformUtil.Path] = [] + frontier: List[TransformUtil.Path] = [port] # use BFS to maintain ordering instead of simpler DFS + while frontier: + pin = frontier.pop(0) + if pin not in seen: + seen.add(pin) + curr_net.append(pin) + frontier.extend(scope.edges[pin]) + nets.append(curr_net) + + pin_to_net: Dict[TransformUtil.Path, List[TransformUtil.Path]] = {} # values share reference to nets + for net in nets: + for pin in net: + pin_to_net[pin] = net + + for connected1, connected2 in scope.assert_connected: + if pin_to_net[connected1] is not pin_to_net[connected2]: + raise InvalidPackingException(f"packed pins {connected1}, {connected2} not connected") + + board_refdes_prefix = self.design.get_value(("refdes_prefix",)) + if board_refdes_prefix is not None: + assert isinstance(board_refdes_prefix, str) + net_prefix = board_refdes_prefix else: - raise ValueError("mismatched result types") - return 0 - best_path = sorted(net, key=cmp_to_key(pin_name_goodness))[0] - - return net_prefix + str(best_path) - - def scope_to_netlist(self, scope: BoardScope) -> Netlist: - # Convert to the netlist format - seen: Set[TransformUtil.Path] = set() - nets: List[List[TransformUtil.Path]] = [] # lists preserve ordering - - for port, conns in scope.edges.items(): - if port not in seen: - curr_net: List[TransformUtil.Path] = [] - frontier: List[TransformUtil.Path] = [port] # use BFS to maintain ordering instead of simpler DFS - while frontier: - pin = frontier.pop(0) - if pin not in seen: - seen.add(pin) - curr_net.append(pin) - frontier.extend(scope.edges[pin]) - nets.append(curr_net) - - pin_to_net: Dict[TransformUtil.Path, List[TransformUtil.Path]] = {} # values share reference to nets - for net in nets: - for pin in net: - pin_to_net[pin] = net - - for (connected1, connected2) in scope.assert_connected: - if pin_to_net[connected1] is not pin_to_net[connected2]: - raise InvalidPackingException(f"packed pins {connected1}, {connected2} not connected") - - board_refdes_prefix = self.design.get_value(('refdes_prefix',)) - if board_refdes_prefix is not None: - assert isinstance(board_refdes_prefix, str) - net_prefix = board_refdes_prefix - else: - net_prefix = '' - named_nets = {self.name_net(net, net_prefix): net for net in nets} - - def port_ignored_paths(path: TransformUtil.Path) -> bool: # ignore link ports for netlisting - return bool(path.links) or any([block.startswith('(adapter)') or block.startswith('(bridge)') for block in path.blocks]) - - netlist_footprints = [footprint for path, footprint in scope.footprints.items()] - netlist_nets = [Net(name, - list(chain(*[scope.pins[port] for port in net if port in scope.pins])), - [port for port in net if not port_ignored_paths(port)]) - for name, net in named_nets.items()] - netlist_nets = [net for net in netlist_nets if net.pins] # prune empty nets - - return Netlist(netlist_footprints, netlist_nets) - - def run(self) -> Netlist: - self.transform_design(self.design.design) - - return self.scope_to_netlist(self.all_scopes[0]) # TODO support multiple scopes + net_prefix = "" + named_nets = {self.name_net(net, net_prefix): net for net in nets} + + def port_ignored_paths(path: TransformUtil.Path) -> bool: # ignore link ports for netlisting + return bool(path.links) or any( + [block.startswith("(adapter)") or block.startswith("(bridge)") for block in path.blocks] + ) + + netlist_footprints = [footprint for path, footprint in scope.footprints.items()] + netlist_nets = [ + Net( + name, + list(chain(*[scope.pins[port] for port in net if port in scope.pins])), + [port for port in net if not port_ignored_paths(port)], + ) + for name, net in named_nets.items() + ] + netlist_nets = [net for net in netlist_nets if net.pins] # prune empty nets + + return Netlist(netlist_footprints, netlist_nets) + + def run(self) -> Netlist: + self.transform_design(self.design.design) + + return self.scope_to_netlist(self.all_scopes[0]) # TODO support multiple scopes diff --git a/edg/electronics_model/PartParserUtil.py b/edg/electronics_model/PartParserUtil.py index 08bef2b34..1c9c1671e 100644 --- a/edg/electronics_model/PartParserUtil.py +++ b/edg/electronics_model/PartParserUtil.py @@ -7,90 +7,91 @@ class PartParserUtil: - """Collection of utilities for parsing part values, eg for reading in schematics - or for parsing part tables.""" + """Collection of utilities for parsing part values, eg for reading in schematics + or for parsing part tables.""" - class ParseError(Exception): - pass + class ParseError(Exception): + pass - SI_PREFIX_DICT = { - 'p': 1e-12, - 'n': 1e-9, - 'μ': 1e-6, - 'µ': 1e-6, - 'u': 1e-6, - 'm': 1e-3, - 'k': 1e3, - 'M': 1e6, - 'G': 1e9, - } - SI_PREFIXES = ''.join(SI_PREFIX_DICT.keys()) + SI_PREFIX_DICT = { + "p": 1e-12, + "n": 1e-9, + "μ": 1e-6, + "µ": 1e-6, + "u": 1e-6, + "m": 1e-3, + "k": 1e3, + "M": 1e6, + "G": 1e9, + } + SI_PREFIXES = "".join(SI_PREFIX_DICT.keys()) - VALUE_REGEX = re.compile(f'^([\d./]+)\s*([{SI_PREFIXES}]?)(.*)$') - DefaultType = TypeVar('DefaultType') - @classmethod - def parse_value(cls, value: str, units: str) -> float: - """Parses a value with unit and SI prefixes, for example '20 nF' would be parsed as 20e-9. - Supports inline prefix notation (eg, 2k2R) and fractional notation (eg, 1/16W) - If the input is not a value: - if default is not specified, raises a ParseError. - if default is specified, returns the default.""" - value = value.strip() - # validate units - if not value.endswith(units): - raise cls.ParseError(f"'{value}' does not have expected units '{units}'") - value = value.removesuffix(units) - # do not re-strip here, prefix must directly precede units - prefix: Optional[str] = None - if value[-1] in cls.SI_PREFIX_DICT.keys(): - prefix = value[-1] - value = value[:-1] - value = value.strip() # allow a space between the value and prefix + units + VALUE_REGEX = re.compile(f"^([\d./]+)\s*([{SI_PREFIXES}]?)(.*)$") + DefaultType = TypeVar("DefaultType") - # at this point, only the numeric part remains (possibly with inline prefix, like 2k2) - if '/' in value: # fractional case - fractional_components = value.split('/') - if len(fractional_components) != 2: - raise cls.ParseError(f"'{value}' has invalid fractional format") - try: - numeric_value = float(fractional_components[0]) / float(fractional_components[1]) - except ValueError: - raise cls.ParseError(f"'{value}' is not a valid fraction") - else: # numeric case, possibly with inline prefix - if value.isnumeric(): - numeric_value = float(value) - else: # check for inline prefix - for test_prefix in cls.SI_PREFIX_DICT.keys(): - if test_prefix in value: - value = value.replace(test_prefix, '.', 1) # only replace the first one - if prefix is not None: - raise cls.ParseError(f"'{value}' contains multiple SI prefixes") - prefix = test_prefix - try: - numeric_value = float(value) - except ValueError: - raise cls.ParseError(f"'{value}' is not numeric") - if prefix is not None: - return numeric_value * cls.SI_PREFIX_DICT[prefix] - else: - return numeric_value + @classmethod + def parse_value(cls, value: str, units: str) -> float: + """Parses a value with unit and SI prefixes, for example '20 nF' would be parsed as 20e-9. + Supports inline prefix notation (eg, 2k2R) and fractional notation (eg, 1/16W) + If the input is not a value: + if default is not specified, raises a ParseError. + if default is specified, returns the default.""" + value = value.strip() + # validate units + if not value.endswith(units): + raise cls.ParseError(f"'{value}' does not have expected units '{units}'") + value = value.removesuffix(units) + # do not re-strip here, prefix must directly precede units + prefix: Optional[str] = None + if value[-1] in cls.SI_PREFIX_DICT.keys(): + prefix = value[-1] + value = value[:-1] + value = value.strip() # allow a space between the value and prefix + units - @classmethod - def parse_abs_tolerance(cls, value: str, center: float, units: str) -> Range: - """Parses a tolerance value and returns the tolerance value a range. - String may not have leading or trailing whitespace, but may have whitespace between parts.""" - if value.startswith('±'): - value = value.removeprefix('±') - else: - raise cls.ParseError(f"Unknown prefix for tolerance '{value}'") + # at this point, only the numeric part remains (possibly with inline prefix, like 2k2) + if "/" in value: # fractional case + fractional_components = value.split("/") + if len(fractional_components) != 2: + raise cls.ParseError(f"'{value}' has invalid fractional format") + try: + numeric_value = float(fractional_components[0]) / float(fractional_components[1]) + except ValueError: + raise cls.ParseError(f"'{value}' is not a valid fraction") + else: # numeric case, possibly with inline prefix + if value.isnumeric(): + numeric_value = float(value) + else: # check for inline prefix + for test_prefix in cls.SI_PREFIX_DICT.keys(): + if test_prefix in value: + value = value.replace(test_prefix, ".", 1) # only replace the first one + if prefix is not None: + raise cls.ParseError(f"'{value}' contains multiple SI prefixes") + prefix = test_prefix + try: + numeric_value = float(value) + except ValueError: + raise cls.ParseError(f"'{value}' is not numeric") + if prefix is not None: + return numeric_value * cls.SI_PREFIX_DICT[prefix] + else: + return numeric_value - if value.endswith('%'): - value = value.removesuffix('%').rstrip() - return Range.from_tolerance(center, float(value) / 100) - elif value.endswith('ppm'): - value = value.removesuffix('ppm').rstrip() - return Range.from_tolerance(center, float(value) * 1e-6) - elif units and value.endswith(units): - return Range.from_abs_tolerance(center, cls.parse_value(value, units)) + @classmethod + def parse_abs_tolerance(cls, value: str, center: float, units: str) -> Range: + """Parses a tolerance value and returns the tolerance value a range. + String may not have leading or trailing whitespace, but may have whitespace between parts.""" + if value.startswith("±"): + value = value.removeprefix("±") + else: + raise cls.ParseError(f"Unknown prefix for tolerance '{value}'") - raise cls.ParseError(f"Unknown tolerance '{value}'") + if value.endswith("%"): + value = value.removesuffix("%").rstrip() + return Range.from_tolerance(center, float(value) / 100) + elif value.endswith("ppm"): + value = value.removesuffix("ppm").rstrip() + return Range.from_tolerance(center, float(value) * 1e-6) + elif units and value.endswith(units): + return Range.from_abs_tolerance(center, cls.parse_value(value, units)) + + raise cls.ParseError(f"Unknown tolerance '{value}'") diff --git a/edg/electronics_model/PassivePort.py b/edg/electronics_model/PassivePort.py index f50e9177e..91850e338 100644 --- a/edg/electronics_model/PassivePort.py +++ b/edg/electronics_model/PassivePort.py @@ -11,151 +11,199 @@ class PassiveLink(CircuitLink): - """Copper-only connection""" - def __init__(self) -> None: - super().__init__() - self.passives = self.Port(Vector(Passive())) + """Copper-only connection""" + + def __init__(self) -> None: + super().__init__() + self.passives = self.Port(Vector(Passive())) class PassiveAdapterGround(CircuitPortAdapter[Ground]): - def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL): - super().__init__() - self.src = self.Port(Passive()) - self.dst = self.Port(Ground(voltage_limits=voltage_limits)) + def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL): + super().__init__() + self.src = self.Port(Passive()) + self.dst = self.Port(Ground(voltage_limits=voltage_limits)) class PassiveAdapterVoltageSource(CircuitPortAdapter[VoltageSource]): - # TODO we can't use **kwargs b/c init_in_parent needs the initializer list - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL): - super().__init__() - self.src = self.Port(Passive()) - self.dst = self.Port(VoltageSource(voltage_out=voltage_out, current_limits=current_limits)) + # TODO we can't use **kwargs b/c init_in_parent needs the initializer list + def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, current_limits: RangeLike = RangeExpr.ALL): + super().__init__() + self.src = self.Port(Passive()) + self.dst = self.Port(VoltageSource(voltage_out=voltage_out, current_limits=current_limits)) class PassiveAdapterVoltageSink(CircuitPortAdapter[VoltageSink]): - # TODO we can't use **kwargs b/c the init hook needs an initializer list - def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO): - super().__init__() - self.src = self.Port(Passive()) - self.dst = self.Port(VoltageSink(voltage_limits=voltage_limits, current_draw=current_draw)) + # TODO we can't use **kwargs b/c the init hook needs an initializer list + def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, current_draw: RangeLike = RangeExpr.ZERO): + super().__init__() + self.src = self.Port(Passive()) + self.dst = self.Port(VoltageSink(voltage_limits=voltage_limits, current_draw=current_draw)) class PassiveAdapterDigitalSource(CircuitPortAdapter[DigitalSource]): - # TODO we can't use **kwargs b/c the init hook needs an initializer list - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL, - output_thresholds: RangeLike = RangeExpr.ALL, - pullup_capable: BoolLike = False, - pulldown_capable: BoolLike = False, - high_driver: BoolLike = True, - low_driver: BoolLike = True, - _bridged_internal: BoolLike = False): - super().__init__() - self.src = self.Port(Passive()) - self.dst = self.Port(DigitalSource(voltage_out=voltage_out, current_limits=current_limits, - output_thresholds=output_thresholds, - pullup_capable=pullup_capable, pulldown_capable=pulldown_capable, - high_driver=high_driver, low_driver=low_driver, - _bridged_internal=_bridged_internal)) + # TODO we can't use **kwargs b/c the init hook needs an initializer list + def __init__( + self, + voltage_out: RangeLike = RangeExpr.ZERO, + current_limits: RangeLike = RangeExpr.ALL, + output_thresholds: RangeLike = RangeExpr.ALL, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + high_driver: BoolLike = True, + low_driver: BoolLike = True, + _bridged_internal: BoolLike = False, + ): + super().__init__() + self.src = self.Port(Passive()) + self.dst = self.Port( + DigitalSource( + voltage_out=voltage_out, + current_limits=current_limits, + output_thresholds=output_thresholds, + pullup_capable=pullup_capable, + pulldown_capable=pulldown_capable, + high_driver=high_driver, + low_driver=low_driver, + _bridged_internal=_bridged_internal, + ) + ) class PassiveAdapterDigitalSink(CircuitPortAdapter[DigitalSink]): - # TODO we can't use **kwargs b/c the init hook needs an initializer list - def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO, - input_thresholds: RangeLike = RangeExpr.EMPTY, - pullup_capable: BoolLike = False, - pulldown_capable: BoolLike = False, - _bridged_internal: BoolLike = False): - super().__init__() - self.src = self.Port(Passive()) - self.dst = self.Port(DigitalSink(voltage_limits=voltage_limits, current_draw=current_draw, - input_thresholds=input_thresholds, - pullup_capable=pullup_capable, - pulldown_capable=pulldown_capable, - _bridged_internal=_bridged_internal)) + # TODO we can't use **kwargs b/c the init hook needs an initializer list + def __init__( + self, + voltage_limits: RangeLike = RangeExpr.ALL, + current_draw: RangeLike = RangeExpr.ZERO, + input_thresholds: RangeLike = RangeExpr.EMPTY, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + _bridged_internal: BoolLike = False, + ): + super().__init__() + self.src = self.Port(Passive()) + self.dst = self.Port( + DigitalSink( + voltage_limits=voltage_limits, + current_draw=current_draw, + input_thresholds=input_thresholds, + pullup_capable=pullup_capable, + pulldown_capable=pulldown_capable, + _bridged_internal=_bridged_internal, + ) + ) class PassiveAdapterDigitalBidir(CircuitPortAdapter[DigitalBidir]): - # TODO we can't use **kwargs b/c the init hook needs an initializer list - def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO, - voltage_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL, - input_thresholds: RangeLike = RangeExpr.EMPTY, - output_thresholds: RangeLike = RangeExpr.ALL, - *, - pullup_capable: BoolLike = False, - pulldown_capable: BoolLike = False, - _bridged_internal: BoolLike = False): - super().__init__() - self.src = self.Port(Passive()) - self.dst = self.Port(DigitalBidir(voltage_limits=voltage_limits, current_draw=current_draw, - voltage_out=voltage_out, current_limits=current_limits, - input_thresholds=input_thresholds, output_thresholds=output_thresholds, - pullup_capable=pullup_capable, pulldown_capable=pulldown_capable, - _bridged_internal=_bridged_internal)) + # TODO we can't use **kwargs b/c the init hook needs an initializer list + def __init__( + self, + voltage_limits: RangeLike = RangeExpr.ALL, + current_draw: RangeLike = RangeExpr.ZERO, + voltage_out: RangeLike = RangeExpr.ZERO, + current_limits: RangeLike = RangeExpr.ALL, + input_thresholds: RangeLike = RangeExpr.EMPTY, + output_thresholds: RangeLike = RangeExpr.ALL, + *, + pullup_capable: BoolLike = False, + pulldown_capable: BoolLike = False, + _bridged_internal: BoolLike = False, + ): + super().__init__() + self.src = self.Port(Passive()) + self.dst = self.Port( + DigitalBidir( + voltage_limits=voltage_limits, + current_draw=current_draw, + voltage_out=voltage_out, + current_limits=current_limits, + input_thresholds=input_thresholds, + output_thresholds=output_thresholds, + pullup_capable=pullup_capable, + pulldown_capable=pulldown_capable, + _bridged_internal=_bridged_internal, + ) + ) class PassiveAdapterAnalogSource(CircuitPortAdapter[AnalogSource]): - # TODO we can't use **kwargs b/c the init hook needs an initializer list - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, signal_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL, impedance: RangeLike = RangeExpr.ZERO): - super().__init__() - self.src = self.Port(Passive()) - self.dst = self.Port(AnalogSource(voltage_out=voltage_out, signal_out=signal_out, - current_limits=current_limits, impedance=impedance)) + # TODO we can't use **kwargs b/c the init hook needs an initializer list + def __init__( + self, + voltage_out: RangeLike = RangeExpr.ZERO, + signal_out: RangeLike = RangeExpr.ZERO, + current_limits: RangeLike = RangeExpr.ALL, + impedance: RangeLike = RangeExpr.ZERO, + ): + super().__init__() + self.src = self.Port(Passive()) + self.dst = self.Port( + AnalogSource( + voltage_out=voltage_out, signal_out=signal_out, current_limits=current_limits, impedance=impedance + ) + ) class PassiveAdapterAnalogSink(CircuitPortAdapter[AnalogSink]): - # TODO we can't use **kwargs b/c the init hook needs an initializer list - def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, signal_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO, - impedance: RangeLike = RangeExpr.INF): - super().__init__() - self.src = self.Port(Passive()) - self.dst = self.Port(AnalogSink(voltage_limits=voltage_limits, signal_limits=signal_limits, - current_draw=current_draw, impedance=impedance)) + # TODO we can't use **kwargs b/c the init hook needs an initializer list + def __init__( + self, + voltage_limits: RangeLike = RangeExpr.ALL, + signal_limits: RangeLike = RangeExpr.ALL, + current_draw: RangeLike = RangeExpr.ZERO, + impedance: RangeLike = RangeExpr.INF, + ): + super().__init__() + self.src = self.Port(Passive()) + self.dst = self.Port( + AnalogSink( + voltage_limits=voltage_limits, + signal_limits=signal_limits, + current_draw=current_draw, + impedance=impedance, + ) + ) class PassiveBridge(CircuitPortBridge): - def __init__(self) -> None: - super().__init__() - self.outer_port = self.Port(Passive()) - self.inner_link = self.Port(Passive()) + def __init__(self) -> None: + super().__init__() + self.outer_port = self.Port(Passive()) + self.inner_link = self.Port(Passive()) class Passive(CircuitPort[PassiveLink]): - """Basic copper-only port, which can be adapted to a more strongly typed Voltage/Digital/Analog* port""" - adapter_type_map: Dict[Type[Port], Type[CircuitPortAdapter]] = { - Ground: PassiveAdapterGround, - VoltageSource: PassiveAdapterVoltageSource, - VoltageSink: PassiveAdapterVoltageSink, - DigitalSink: PassiveAdapterDigitalSink, - DigitalSource: PassiveAdapterDigitalSource, - DigitalBidir: PassiveAdapterDigitalBidir, - AnalogSink: PassiveAdapterAnalogSink, - AnalogSource: PassiveAdapterAnalogSource - } - link_type = PassiveLink - bridge_type = PassiveBridge - - AdaptTargetType = TypeVar('AdaptTargetType', bound=CircuitPort) - def adapt_to(self, that: AdaptTargetType) -> AdaptTargetType: - # this is an experimental style that takes a port that has initializers but is not bound - # and automatically creates an adapter from it, by matching the port parameter fields - # with the adapter constructor argument fields by name - assert isinstance(that, Port), 'adapter target must be port' - assert not that._is_bound(), 'adapter target must be model only' - assert that.__class__ in self.adapter_type_map, f'no adapter to {that.__class__}' - adapter_cls = self.adapter_type_map[that.__class__] - - # map initializers from that to constructor args - adapter_init_kwargs = {} # make everything kwargs for simplicity - for param_name, param in that._parameters.items(): - assert param.initializer is not None, f"missing initializer for {param_name}" - adapter_init_kwargs[param_name] = param.initializer - - return self._convert(adapter_cls(**adapter_init_kwargs)) # type: ignore + """Basic copper-only port, which can be adapted to a more strongly typed Voltage/Digital/Analog* port""" + + adapter_type_map: Dict[Type[Port], Type[CircuitPortAdapter]] = { + Ground: PassiveAdapterGround, + VoltageSource: PassiveAdapterVoltageSource, + VoltageSink: PassiveAdapterVoltageSink, + DigitalSink: PassiveAdapterDigitalSink, + DigitalSource: PassiveAdapterDigitalSource, + DigitalBidir: PassiveAdapterDigitalBidir, + AnalogSink: PassiveAdapterAnalogSink, + AnalogSource: PassiveAdapterAnalogSource, + } + link_type = PassiveLink + bridge_type = PassiveBridge + + AdaptTargetType = TypeVar("AdaptTargetType", bound=CircuitPort) + + def adapt_to(self, that: AdaptTargetType) -> AdaptTargetType: + # this is an experimental style that takes a port that has initializers but is not bound + # and automatically creates an adapter from it, by matching the port parameter fields + # with the adapter constructor argument fields by name + assert isinstance(that, Port), "adapter target must be port" + assert not that._is_bound(), "adapter target must be model only" + assert that.__class__ in self.adapter_type_map, f"no adapter to {that.__class__}" + adapter_cls = self.adapter_type_map[that.__class__] + + # map initializers from that to constructor args + adapter_init_kwargs = {} # make everything kwargs for simplicity + for param_name, param in that._parameters.items(): + assert param.initializer is not None, f"missing initializer for {param_name}" + adapter_init_kwargs[param_name] = param.initializer + + return self._convert(adapter_cls(**adapter_init_kwargs)) # type: ignore diff --git a/edg/electronics_model/PinAssignmentUtil.py b/edg/electronics_model/PinAssignmentUtil.py index f836f1f32..dd58bcbfa 100644 --- a/edg/electronics_model/PinAssignmentUtil.py +++ b/edg/electronics_model/PinAssignmentUtil.py @@ -9,17 +9,16 @@ def leaf_circuit_ports(prefix: str, port: Port) -> Iterable[Tuple[str, CircuitPort]]: - if isinstance(port, CircuitPort): - return [(prefix, port)] - elif isinstance(port, Bundle): - return chain(*[leaf_circuit_ports(f"{prefix}.{name}", port) - for (name, port) in port._ports.items()]) - else: - raise ValueError(f"unable to flatten {port}") + if isinstance(port, CircuitPort): + return [(prefix, port)] + elif isinstance(port, Bundle): + return chain(*[leaf_circuit_ports(f"{prefix}.{name}", port) for (name, port) in port._ports.items()]) + else: + raise ValueError(f"unable to flatten {port}") -class SpecialPin(): - pass +class SpecialPin: + pass NotConnectedPin = SpecialPin() @@ -30,131 +29,142 @@ class SpecialPin(): class AssignedPins(NamedTuple): - assigned_pins: Dict[ConcretePinName, CircuitPort] # map of internal pin name -> edge leaf port - not_connected: List[CircuitPort] # list of edge leaf ports that are marked as NC + assigned_pins: Dict[ConcretePinName, CircuitPort] # map of internal pin name -> edge leaf port + not_connected: List[CircuitPort] # list of edge leaf ports that are marked as NC -class AssignablePinGroup(): - """Base class for assignable pin definitions""" - @abstractmethod - def get_assignable_ports(self) -> Iterable[Port]: ... # returns all top-level assignable ports +class AssignablePinGroup: + """Base class for assignable pin definitions""" - @abstractmethod - def assign(self, port: Port, preassigns: IdentityDict[CircuitPort, PinName], assigned: Set[ConcretePinName]) ->\ - Optional[AssignedPins]: ... # returns an assignment of all leaf ports given a top-level port + @abstractmethod + def get_assignable_ports(self) -> Iterable[Port]: ... # returns all top-level assignable ports + + @abstractmethod + def assign( + self, port: Port, preassigns: IdentityDict[CircuitPort, PinName], assigned: Set[ConcretePinName] + ) -> Optional[AssignedPins]: ... # returns an assignment of all leaf ports given a top-level port class AnyPinAssign(AssignablePinGroup): - """Pin assignment where any leaf port can be connected to any pin. - All leaf ports must be CircuitPort. - Useful for devices with a switch matrix, or for assigning GPIOs""" - def __init__(self, ports: Iterable[Port], pins: Iterable[Union[str, int]]) -> None: - self.all_ports = list(ports) - self.pins = set(str(pin) for pin in pins) - - @override - def get_assignable_ports(self) -> Iterable[Port]: - return self.all_ports - - @override - def assign(self, port: Port, preassigns: IdentityDict[CircuitPort, PinName], assigned: Set[ConcretePinName]) ->\ - Optional[AssignedPins]: - assignments: Dict[ConcretePinName, CircuitPort] = {} - not_connected: List[CircuitPort] = [] - available_pins = self.pins.difference(assigned).difference(preassigns.values()) - for leaf_name, leaf_port in leaf_circuit_ports("", port): - if leaf_port in preassigns: - preassign_pin = preassigns[leaf_port] - if isinstance(preassign_pin, str): # shouldn't have ints based on how params propagated - assert preassign_pin in self.pins, f"preassign for {preassign_pin}={port} not in pin set {self.pins} for {self}" - assignments[preassign_pin] = leaf_port - elif preassign_pin is NotConnectedPin: - not_connected.append(leaf_port) - else: - raise ValueError(f"bad preassign value {preassign_pin}") - elif not available_pins: - return None - else: - assignments[available_pins.pop()] = leaf_port - return AssignedPins(assigned_pins=assignments, not_connected=not_connected) + """Pin assignment where any leaf port can be connected to any pin. + All leaf ports must be CircuitPort. + Useful for devices with a switch matrix, or for assigning GPIOs""" + + def __init__(self, ports: Iterable[Port], pins: Iterable[Union[str, int]]) -> None: + self.all_ports = list(ports) + self.pins = set(str(pin) for pin in pins) + + @override + def get_assignable_ports(self) -> Iterable[Port]: + return self.all_ports + + @override + def assign( + self, port: Port, preassigns: IdentityDict[CircuitPort, PinName], assigned: Set[ConcretePinName] + ) -> Optional[AssignedPins]: + assignments: Dict[ConcretePinName, CircuitPort] = {} + not_connected: List[CircuitPort] = [] + available_pins = self.pins.difference(assigned).difference(preassigns.values()) + for leaf_name, leaf_port in leaf_circuit_ports("", port): + if leaf_port in preassigns: + preassign_pin = preassigns[leaf_port] + if isinstance(preassign_pin, str): # shouldn't have ints based on how params propagated + assert ( + preassign_pin in self.pins + ), f"preassign for {preassign_pin}={port} not in pin set {self.pins} for {self}" + assignments[preassign_pin] = leaf_port + elif preassign_pin is NotConnectedPin: + not_connected.append(leaf_port) + else: + raise ValueError(f"bad preassign value {preassign_pin}") + elif not available_pins: + return None + else: + assignments[available_pins.pop()] = leaf_port + return AssignedPins(assigned_pins=assignments, not_connected=not_connected) class PeripheralPinAssign(AssignablePinGroup): - """Pin assignment where the Bundle must be assigned as a whole. - Remappable leaf nodes are permitted. - Each pin list is for one port (eg Bundle), with each element being the pin(s) assignable for that leaf wire.""" - def __init__(self, ports: Iterable[Port], *pin_groups: List[Union[List[Union[int, str]], int, str]]) -> None: - self.all_ports = list(ports) - def process_assignable_pin(leaf_pins: Union[List[Union[int, str]], int, str]) -> List[str]: - if isinstance(leaf_pins, list): - return [str(pin) for pin in leaf_pins] - else: - return [str(leaf_pins)] - - self.pins: List[List[List[str]]] = [ - [process_assignable_pin(leaf_pins) for leaf_pins in pin_group] for pin_group in pin_groups - ] - - @override - def get_assignable_ports(self) -> Iterable[Port]: - return self.all_ports - - @override - def assign(self, port: Port, preassigns: IdentityDict[CircuitPort, PinName], assigned: Set[ConcretePinName]) -> \ - Optional[AssignedPins]: - leaf_name_ports = list(leaf_circuit_ports("", port)) - - for group_pins in self.pins: - assert len(group_pins) == len(leaf_name_ports) - - group_dict: Dict[ConcretePinName, CircuitPort] = {} - not_connected: List[CircuitPort] = [] - group_failed = False - for available_pins, (leaf_name, leaf_port) in zip(group_pins, leaf_name_ports): - if group_failed: - continue - - if leaf_port in preassigns and preassigns[leaf_port]: # preassigned pin - preassign_pin = preassigns[leaf_port] - - if preassign_pin is NotConnectedPin: - not_connected.append(leaf_port) - elif isinstance(preassign_pin, str): - if preassign_pin in available_pins: - group_dict[preassign_pin] = leaf_port - else: - group_failed = True - else: - raise ValueError(f"bad preassign value {preassign_pin}") - else: # non-preassigned pin - available_pins = [pin for pin in available_pins if pin not in assigned] - if available_pins: - group_dict[available_pins[0]] = leaf_port - else: - group_failed = True + """Pin assignment where the Bundle must be assigned as a whole. + Remappable leaf nodes are permitted. + Each pin list is for one port (eg Bundle), with each element being the pin(s) assignable for that leaf wire.""" - if not group_failed: - return AssignedPins(assigned_pins=group_dict, not_connected=not_connected) - return None + def __init__(self, ports: Iterable[Port], *pin_groups: List[Union[List[Union[int, str]], int, str]]) -> None: + self.all_ports = list(ports) + + def process_assignable_pin(leaf_pins: Union[List[Union[int, str]], int, str]) -> List[str]: + if isinstance(leaf_pins, list): + return [str(pin) for pin in leaf_pins] + else: + return [str(leaf_pins)] + + self.pins: List[List[List[str]]] = [ + [process_assignable_pin(leaf_pins) for leaf_pins in pin_group] for pin_group in pin_groups + ] + + @override + def get_assignable_ports(self) -> Iterable[Port]: + return self.all_ports + + @override + def assign( + self, port: Port, preassigns: IdentityDict[CircuitPort, PinName], assigned: Set[ConcretePinName] + ) -> Optional[AssignedPins]: + leaf_name_ports = list(leaf_circuit_ports("", port)) + + for group_pins in self.pins: + assert len(group_pins) == len(leaf_name_ports) + + group_dict: Dict[ConcretePinName, CircuitPort] = {} + not_connected: List[CircuitPort] = [] + group_failed = False + for available_pins, (leaf_name, leaf_port) in zip(group_pins, leaf_name_ports): + if group_failed: + continue + + if leaf_port in preassigns and preassigns[leaf_port]: # preassigned pin + preassign_pin = preassigns[leaf_port] + + if preassign_pin is NotConnectedPin: + not_connected.append(leaf_port) + elif isinstance(preassign_pin, str): + if preassign_pin in available_pins: + group_dict[preassign_pin] = leaf_port + else: + group_failed = True + else: + raise ValueError(f"bad preassign value {preassign_pin}") + else: # non-preassigned pin + available_pins = [pin for pin in available_pins if pin not in assigned] + if available_pins: + group_dict[available_pins[0]] = leaf_port + else: + group_failed = True + + if not group_failed: + return AssignedPins(assigned_pins=group_dict, not_connected=not_connected) + return None class PinAssignmentUtil: - """Utility for pin assignment, given a list of ports and assignable pin definitions, assigns each leaf CircuitPort - port to a pin.""" - def __init__(self, *assignables: AssignablePinGroup) -> None: - self.assignables_by_port = IdentityDict[Port, AssignablePinGroup]() - - for assignable in assignables: - for port in assignable.get_assignable_ports(): - self.assignables_by_port[port] = assignable - - def assign(self, ports: Iterable[Port], preassigns: IdentityDict[CircuitPort, PinName] = IdentityDict()) ->\ - AssignedPins: - assignments: Dict[ConcretePinName, CircuitPort] = {} - not_connects: List[CircuitPort] = [] - for port in ports: - port_assignment = self.assignables_by_port[port].assign(port, preassigns, set(assignments.keys())) - assert port_assignment is not None # TODO more robust and backtracking - assignments.update(port_assignment.assigned_pins) - not_connects.extend(port_assignment.not_connected) - return AssignedPins(assigned_pins=assignments, not_connected=not_connects) + """Utility for pin assignment, given a list of ports and assignable pin definitions, assigns each leaf CircuitPort + port to a pin.""" + + def __init__(self, *assignables: AssignablePinGroup) -> None: + self.assignables_by_port = IdentityDict[Port, AssignablePinGroup]() + + for assignable in assignables: + for port in assignable.get_assignable_ports(): + self.assignables_by_port[port] = assignable + + def assign( + self, ports: Iterable[Port], preassigns: IdentityDict[CircuitPort, PinName] = IdentityDict() + ) -> AssignedPins: + assignments: Dict[ConcretePinName, CircuitPort] = {} + not_connects: List[CircuitPort] = [] + for port in ports: + port_assignment = self.assignables_by_port[port].assign(port, preassigns, set(assignments.keys())) + assert port_assignment is not None # TODO more robust and backtracking + assignments.update(port_assignment.assigned_pins) + not_connects.extend(port_assignment.not_connected) + return AssignedPins(assigned_pins=assignments, not_connected=not_connects) diff --git a/edg/electronics_model/RefdesRefinementPass.py b/edg/electronics_model/RefdesRefinementPass.py index 9dded55a8..d1ef12fbb 100644 --- a/edg/electronics_model/RefdesRefinementPass.py +++ b/edg/electronics_model/RefdesRefinementPass.py @@ -8,49 +8,51 @@ class RefdesRefinementPass(BaseRefinementPass): - @override - def run(self, design: CompiledDesign) -> List[Tuple[edgir.LocalPath, edgir.ValueLit]]: - block_refdes_list = RefdesTransform(design).run() - return [(block_path.append_param('fp_refdes').to_local_path(), edgir.lit_to_valuelit(block_refdes)) - for (block_path, block_refdes) in block_refdes_list] + @override + def run(self, design: CompiledDesign) -> List[Tuple[edgir.LocalPath, edgir.ValueLit]]: + block_refdes_list = RefdesTransform(design).run() + return [ + (block_path.append_param("fp_refdes").to_local_path(), edgir.lit_to_valuelit(block_refdes)) + for (block_path, block_refdes) in block_refdes_list + ] class RefdesTransform(TransformUtil.Transform): - def __init__(self, design: CompiledDesign): - self.design = design - - board_refdes_prefix = self.design.get_value(('refdes_prefix',)) - if board_refdes_prefix is None: - self.board_refdes_prefix = '' - else: - assert isinstance(board_refdes_prefix, str) - self.board_refdes_prefix = board_refdes_prefix - - self.scopes: Dict[TransformUtil.Path, Optional[TransformUtil.Path]] = { - TransformUtil.Path.empty(): TransformUtil.Path.empty() - } - - self.block_refdes_list: List[Tuple[TransformUtil.Path, str]] = [] # populated in traversal order - self.refdes_last: Dict[Tuple[TransformUtil.Path, str], int] = {} # (scope, prefix) -> num - - @override - def visit_block(self, context: TransformUtil.TransformContext, block: edgir.BlockTypes) -> None: - scope = self.scopes[context.path] - internal_scope = scope - if 'fp_is_wrapper' in block.meta.members.node: # wrapper internal blocks ignored - internal_scope = None - - for block_pair in block.blocks: - self.scopes[context.path.append_block(block_pair.name)] = internal_scope - - if 'fp_is_footprint' in block.meta.members.node and scope is not None: - refdes_prefix = self.design.get_value(context.path.to_tuple() + ('fp_refdes_prefix',)) - assert isinstance(refdes_prefix, str) - - refdes_id = self.refdes_last.get((scope, refdes_prefix), 0) + 1 - self.refdes_last[(scope, refdes_prefix)] = refdes_id - self.block_refdes_list.append((context.path, self.board_refdes_prefix + refdes_prefix + str(refdes_id))) - - def run(self) -> List[Tuple[TransformUtil.Path, str]]: - self.transform_design(self.design.design) - return self.block_refdes_list + def __init__(self, design: CompiledDesign): + self.design = design + + board_refdes_prefix = self.design.get_value(("refdes_prefix",)) + if board_refdes_prefix is None: + self.board_refdes_prefix = "" + else: + assert isinstance(board_refdes_prefix, str) + self.board_refdes_prefix = board_refdes_prefix + + self.scopes: Dict[TransformUtil.Path, Optional[TransformUtil.Path]] = { + TransformUtil.Path.empty(): TransformUtil.Path.empty() + } + + self.block_refdes_list: List[Tuple[TransformUtil.Path, str]] = [] # populated in traversal order + self.refdes_last: Dict[Tuple[TransformUtil.Path, str], int] = {} # (scope, prefix) -> num + + @override + def visit_block(self, context: TransformUtil.TransformContext, block: edgir.BlockTypes) -> None: + scope = self.scopes[context.path] + internal_scope = scope + if "fp_is_wrapper" in block.meta.members.node: # wrapper internal blocks ignored + internal_scope = None + + for block_pair in block.blocks: + self.scopes[context.path.append_block(block_pair.name)] = internal_scope + + if "fp_is_footprint" in block.meta.members.node and scope is not None: + refdes_prefix = self.design.get_value(context.path.to_tuple() + ("fp_refdes_prefix",)) + assert isinstance(refdes_prefix, str) + + refdes_id = self.refdes_last.get((scope, refdes_prefix), 0) + 1 + self.refdes_last[(scope, refdes_prefix)] = refdes_id + self.block_refdes_list.append((context.path, self.board_refdes_prefix + refdes_prefix + str(refdes_id))) + + def run(self) -> List[Tuple[TransformUtil.Path, str]]: + self.transform_design(self.design.design) + return self.block_refdes_list diff --git a/edg/electronics_model/SpeakerPort.py b/edg/electronics_model/SpeakerPort.py index 7e237ac70..b60b96160 100644 --- a/edg/electronics_model/SpeakerPort.py +++ b/edg/electronics_model/SpeakerPort.py @@ -7,37 +7,37 @@ class SpeakerLink(Link): - def __init__(self) -> None: - super().__init__() - self.source = self.Port(SpeakerDriverPort()) - self.sink = self.Port(SpeakerPort()) + def __init__(self) -> None: + super().__init__() + self.source = self.Port(SpeakerDriverPort()) + self.sink = self.Port(SpeakerPort()) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.a = self.connect(self.source.a, self.sink.a) - self.b = self.connect(self.source.b, self.sink.b) + self.a = self.connect(self.source.a, self.sink.a) + self.b = self.connect(self.source.b, self.sink.b) class SpeakerDriverPort(Bundle[SpeakerLink]): - link_type = SpeakerLink + link_type = SpeakerLink - def __init__(self, model: Optional[AnalogSource] = None) -> None: - super().__init__() + def __init__(self, model: Optional[AnalogSource] = None) -> None: + super().__init__() - if model is None: - model = AnalogSource() # ideal by default - self.a = self.Port(model) - self.b = self.Port(model) + if model is None: + model = AnalogSource() # ideal by default + self.a = self.Port(model) + self.b = self.Port(model) class SpeakerPort(Bundle[SpeakerLink]): - link_type = SpeakerLink - - def __init__(self, model: Optional[AnalogSink] = None) -> None: - super().__init__() - if model is None: - model = AnalogSink() # ideal by default - self.a = self.Port(model) # TODO how to model max power? - self.b = self.Port(model) + link_type = SpeakerLink + + def __init__(self, model: Optional[AnalogSink] = None) -> None: + super().__init__() + if model is None: + model = AnalogSink() # ideal by default + self.a = self.Port(model) # TODO how to model max power? + self.b = self.Port(model) diff --git a/edg/electronics_model/SpiPort.py b/edg/electronics_model/SpiPort.py index 4cf8d5bee..1e8669bd4 100644 --- a/edg/electronics_model/SpiPort.py +++ b/edg/electronics_model/SpiPort.py @@ -7,61 +7,63 @@ class SpiLink(Link): - """Controller/peripheral naming conventions follow https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/, - though SPI naming in general is a mess. - Unlike I2C, there is not an authoritative SPI specification. - Other names exist, including main/subnode (Wikipedia) and controller/target (NXP, following their I2C convention). - - Internal link signal names are not considered part of the stable public API and may change - without a deprecation phase and backwards compatibility. - """ - def __init__(self) -> None: - super().__init__() - self.controller = self.Port(SpiController(DigitalBidir.empty())) - self.peripherals = self.Port(Vector(SpiPeripheral(DigitalBidir.empty()))) - - @override - def contents(self) -> None: - super().contents() - self.sck = self.connect(self.controller.sck, self.peripherals.map_extract(lambda device: device.sck), - flatten=True) - self.miso = self.connect(self.controller.miso, self.peripherals.map_extract(lambda device: device.miso), - flatten=True) - self.mosi = self.connect(self.controller.mosi, self.peripherals.map_extract(lambda device: device.mosi), - flatten=True) + """Controller/peripheral naming conventions follow https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/, + though SPI naming in general is a mess. + Unlike I2C, there is not an authoritative SPI specification. + Other names exist, including main/subnode (Wikipedia) and controller/target (NXP, following their I2C convention). + + Internal link signal names are not considered part of the stable public API and may change + without a deprecation phase and backwards compatibility. + """ + + def __init__(self) -> None: + super().__init__() + self.controller = self.Port(SpiController(DigitalBidir.empty())) + self.peripherals = self.Port(Vector(SpiPeripheral(DigitalBidir.empty()))) + + @override + def contents(self) -> None: + super().contents() + self.sck = self.connect( + self.controller.sck, self.peripherals.map_extract(lambda device: device.sck), flatten=True + ) + self.miso = self.connect( + self.controller.miso, self.peripherals.map_extract(lambda device: device.miso), flatten=True + ) + self.mosi = self.connect( + self.controller.mosi, self.peripherals.map_extract(lambda device: device.mosi), flatten=True + ) class SpiController(Bundle[SpiLink]): - link_type = SpiLink + link_type = SpiLink - def __init__(self, model: Optional[DigitalBidir] = None, - frequency: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.sck = self.Port(DigitalSource.from_bidir(model)) - self.mosi = self.Port(DigitalSource.from_bidir(model)) - self.miso = self.Port(DigitalSink.from_bidir(model)) + def __init__(self, model: Optional[DigitalBidir] = None, frequency: RangeLike = RangeExpr.ZERO) -> None: + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.sck = self.Port(DigitalSource.from_bidir(model)) + self.mosi = self.Port(DigitalSource.from_bidir(model)) + self.miso = self.Port(DigitalSink.from_bidir(model)) - self.frequency = self.Parameter(RangeExpr(frequency)) - self.mode = self.Parameter(RangeExpr()) # modes supported, in [0, 3] TODO: what about sparse modes? + self.frequency = self.Parameter(RangeExpr(frequency)) + self.mode = self.Parameter(RangeExpr()) # modes supported, in [0, 3] TODO: what about sparse modes? class SpiPeripheral(Bundle[SpiLink]): - link_type = SpiLink - - def __init__(self, model: Optional[DigitalBidir] = None, - frequency_limit: RangeLike = RangeExpr.ALL) -> None: - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.sck = self.Port(DigitalSink.from_bidir(model)) - self.mosi = self.Port(DigitalSink.from_bidir(model)) - self.miso = self.Port(model) - # TODO: (?) CS is defined separately - - self.frequency_limit = self.Parameter(RangeExpr(frequency_limit)) # range of acceptable frequencies - self.mode_limit = self.Parameter(RangeExpr()) # range of acceptable modes, in [0, 3] + link_type = SpiLink + + def __init__(self, model: Optional[DigitalBidir] = None, frequency_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.sck = self.Port(DigitalSink.from_bidir(model)) + self.mosi = self.Port(DigitalSink.from_bidir(model)) + self.miso = self.Port(model) + # TODO: (?) CS is defined separately + + self.frequency_limit = self.Parameter(RangeExpr(frequency_limit)) # range of acceptable frequencies + self.mode_limit = self.Parameter(RangeExpr()) # range of acceptable modes, in [0, 3] # legacy names diff --git a/edg/electronics_model/SvgPcbBackend.py b/edg/electronics_model/SvgPcbBackend.py index 64c879a13..8f64c8969 100644 --- a/edg/electronics_model/SvgPcbBackend.py +++ b/edg/electronics_model/SvgPcbBackend.py @@ -16,7 +16,8 @@ class PlacedBlock(NamedTuple): """A placement of a hierarchical block, including the coordinates of its immediate elements. Elements are placed in local space, with (0, 0) as the origin and elements moved as a group. Elements are indexed by name.""" - elts: List[Tuple[Union['PlacedBlock', TransformUtil.Path], Tuple[float, float]]] # name -> elt, (x, y) + + elts: List[Tuple[Union["PlacedBlock", TransformUtil.Path], Tuple[float, float]]] # name -> elt, (x, y) height: float width: float @@ -26,8 +27,7 @@ class BlackBoxBlock(NamedTuple): bbox: Tuple[float, float, float, float] -def arrange_blocks(blocks: List[NetBlock], - additional_blocks: List[BlackBoxBlock] = []) -> PlacedBlock: +def arrange_blocks(blocks: List[NetBlock], additional_blocks: List[BlackBoxBlock] = []) -> PlacedBlock: FOOTPRINT_BORDER = 1 # mm BLOCK_BORDER = 2 # mm @@ -37,7 +37,7 @@ def arrange_blocks(blocks: List[NetBlock], # for here, we only group one level deep for block in blocks: - containing_path = block.full_path.blocks[0:min(len(block.full_path.blocks) - 1, 1)] + containing_path = block.full_path.blocks[0 : min(len(block.full_path.blocks) - 1, 1)] block_footprints.setdefault(containing_path, []).append(block) for i in range(len(containing_path)): subblocks_list = block_subblocks.setdefault(tuple(containing_path[:i]), list()) @@ -45,7 +45,7 @@ def arrange_blocks(blocks: List[NetBlock], subblocks_list.append(containing_path[i]) for blackbox in additional_blocks: - containing_path = blackbox.path.blocks[0:min(len(blackbox.path.blocks) - 1, 1)] + containing_path = blackbox.path.blocks[0 : min(len(blackbox.path.blocks) - 1, 1)] block_footprints.setdefault(containing_path, []).append(blackbox) for i in range(len(containing_path)): subblocks_list = block_subblocks.setdefault(tuple(containing_path[:i]), list()) @@ -58,7 +58,9 @@ def arrange_hierarchy(root: Tuple[str, ...]) -> PlacedBlock: # TODO don't count borders as part of a block's width / height ASPECT_RATIO = 16 / 9 - sub_placed: List[Tuple[float, float, Union[PlacedBlock, NetBlock, BlackBoxBlock]]] = [] # (width, height, entry) + sub_placed: List[Tuple[float, float, Union[PlacedBlock, NetBlock, BlackBoxBlock]]] = ( + [] + ) # (width, height, entry) for subblock in block_subblocks.get(root, list()): subplaced = arrange_hierarchy(root + (subblock,)) sub_placed.append((subplaced.width + BLOCK_BORDER, subplaced.height + BLOCK_BORDER, subplaced)) @@ -118,15 +120,15 @@ def arrange_hierarchy(root: Tuple[str, ...]) -> PlacedBlock: x_stack.append((next_x + width, next_y, next_y + height)) x_max = max(x_max, next_x + width) y_max = max(y_max, next_y + height) - return PlacedBlock( - elts=elts, width=x_max, height=y_max - ) + return PlacedBlock(elts=elts, width=x_max, height=y_max) + return arrange_hierarchy(()) def flatten_packed_block(block: PlacedBlock) -> Dict[TransformUtil.Path, Tuple[float, float]]: """Flatten a packed_block to a dict of individual components.""" flattened: Dict[TransformUtil.Path, Tuple[float, float]] = {} + def walk_group(block: PlacedBlock, x_pos: float, y_pos: float) -> None: for elt, (elt_x, elt_y) in block.elts: if isinstance(elt, PlacedBlock): @@ -135,6 +137,7 @@ def walk_group(block: PlacedBlock, x_pos: float, y_pos: float) -> None: flattened[elt] = (x_pos + elt_x, y_pos + elt_y) else: raise TypeError + walk_group(block, 0, 0) return flattened @@ -148,6 +151,7 @@ class SvgPcbGeneratedBlock(NamedTuple): class SvgPcbTransform(TransformUtil.Transform): """Collects all SVGPCB blocks and initializes them.""" + def __init__(self, design: CompiledDesign, netlist: Netlist): self.design = design self.netlist = netlist @@ -160,16 +164,21 @@ def visit_block(self, context: TransformUtil.TransformContext, block: edgir.Bloc return # TODO: dedup w/ class_from_library in edg_hdl_server - elt_split = block.self_class.target.name.split('.') - elt_module = importlib.import_module('.'.join(elt_split[:-1])) + elt_split = block.self_class.target.name.split(".") + elt_module = importlib.import_module(".".join(elt_split[:-1])) assert inspect.ismodule(elt_module) cls = getattr(elt_module, elt_split[-1]) if issubclass(cls, SvgPcbTemplateBlock): generator_obj = cls() generator_obj._svgpcb_init(context.path, self.design, self.netlist) - self._svgpcb_blocks.append(SvgPcbGeneratedBlock( - context.path, generator_obj._svgpcb_fn_name(), generator_obj._svgpcb_template(), generator_obj._svgpcb_bbox() - )) + self._svgpcb_blocks.append( + SvgPcbGeneratedBlock( + context.path, + generator_obj._svgpcb_fn_name(), + generator_obj._svgpcb_template(), + generator_obj._svgpcb_bbox(), + ) + ) else: pass @@ -183,21 +192,21 @@ class SvgPcbBackend(BaseBackend): def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[edgir.LocalPath, str]]: netlist = NetlistTransform(design).run() result = self._generate(design, netlist) - return [ - (edgir.LocalPath(), result) - ] + return [(edgir.LocalPath(), result)] def _generate(self, design: CompiledDesign, netlist: Netlist) -> str: """Generates SVBPCB fragments as a structured result""" + def block_matches_prefixes(block: NetBlock, prefixes: List[Tuple[str, ...]]) -> bool: for prefix in prefixes: - if block.full_path.blocks[0:min(len(block.full_path.blocks), len(prefix))] == prefix: + if block.full_path.blocks[0 : min(len(block.full_path.blocks), len(prefix))] == prefix: return True return False - def filter_blocks_by_pathname(blocks: List[NetBlock], exclude_prefixes: List[Tuple[str, ...]]) -> List[NetBlock]: - return [block for block in blocks - if not block_matches_prefixes(block, exclude_prefixes)] + def filter_blocks_by_pathname( + blocks: List[NetBlock], exclude_prefixes: List[Tuple[str, ...]] + ) -> List[NetBlock]: + return [block for block in blocks if not block_matches_prefixes(block, exclude_prefixes)] # handle blocks with svgpcb templates svgpcb_blocks = SvgPcbTransform(design, netlist).run() @@ -234,7 +243,7 @@ def filter_blocks_by_pathname(blocks: List[NetBlock], exclude_prefixes: List[Tup pads_code = [f"""["{net_blocks_by_path[pin.block_path].refdes}", "{pin.pin_name}"]""" for pin in net.pins] netlist_code_entries.append(f"""{{name: "{net.name}", pads: [{', '.join(pads_code)}]}}""") - NEWLINE = '\n' + NEWLINE = "\n" full_code = f"""\ const board = new PCB(); diff --git a/edg/electronics_model/SvgPcbTemplateBlock.py b/edg/electronics_model/SvgPcbTemplateBlock.py index 85e90180d..2d3ef3837 100644 --- a/edg/electronics_model/SvgPcbTemplateBlock.py +++ b/edg/electronics_model/SvgPcbTemplateBlock.py @@ -14,13 +14,14 @@ class SvgPcbTemplateBlock(Block): and generation of non-templated footprints exists in the backend. This defines the interface and supporting utilities only.""" + @staticmethod def _svgpcb_pathname_to_svgpcb(path: TransformUtil.Path) -> str: - return '_'.join(path.to_tuple()).replace('[', '_').replace(']', '_') + return "_".join(path.to_tuple()).replace("[", "_").replace("]", "_") @staticmethod def _svgpcb_footprint_to_svgpcb(footprint: str) -> str: # KiCad footprint name to SVGPCB reference - return footprint.split(':')[-1].replace('-', '_').replace(' ', '_').replace('.', '_') + return footprint.split(":")[-1].replace("-", "_").replace(" ", "_").replace(".", "_") def _svgpcb_init(self, pathname: TransformUtil.Path, design: CompiledDesign, netlist: Netlist) -> None: """Initializes this Block for SVGPCB template generation. Called from the backend.""" @@ -45,8 +46,7 @@ def _svgpcb_refdes_of(self, block_ref: List[str]) -> Tuple[str, int]: """Returns the refdes of a block, as a tuple of prefix and number, or crashes if the block is not valid.""" block_path = self._svgpcb_pathname_data.append_block(*block_ref) - candidate_blocks = [block for block in self._svgpcb_netlist.blocks - if block.full_path.startswith(block_path)] + candidate_blocks = [block for block in self._svgpcb_netlist.blocks if block.full_path.startswith(block_path)] assert len(candidate_blocks) > 0 refdes = candidate_blocks[0].refdes assert isinstance(refdes, str) @@ -55,23 +55,21 @@ def _svgpcb_refdes_of(self, block_ref: List[str]) -> Tuple[str, int]: if refdes[i].isalpha(): if i == len(refdes) - 1: return refdes, -1 # fallback if no numeric portion - return refdes[:i+1], int(refdes[i+1:]) + return refdes[: i + 1], int(refdes[i + 1 :]) return "", int(refdes) def _svgpcb_footprint_block_path_of(self, block_ref: List[str]) -> TransformUtil.Path: """Infrastructure method, given the name of a container block, returns the block path of the footprint block. Picks the first one, which is assumed to be the main / anchor device.""" block_path = self._svgpcb_pathname_data.append_block(*block_ref) - candidate_blocks = [block for block in self._svgpcb_netlist.blocks - if block.full_path.startswith(block_path)] + candidate_blocks = [block for block in self._svgpcb_netlist.blocks if block.full_path.startswith(block_path)] assert len(candidate_blocks) > 0 return candidate_blocks[0].full_path def _svgpcb_footprint_of(self, path: TransformUtil.Path) -> str: """Infrastructure method, returns the footprint for the output of _svgpcb_footprint_block_path_of. If _svgpcb_footprint_block_path_of returned a value, this will return the footprint; otherwise crashes.""" - candidate_blocks = [block for block in self._svgpcb_netlist.blocks - if block.full_path == path] + candidate_blocks = [block for block in self._svgpcb_netlist.blocks if block.full_path == path] assert len(candidate_blocks) > 0 return self._svgpcb_footprint_to_svgpcb(candidate_blocks[0].footprint) @@ -80,11 +78,9 @@ def _svgpcb_pin_of(self, block_ref: List[str], pin_ref: List[str]) -> str: be connected to one of its pins, returns the footprint pin that the port is connected to.""" footprint_path = self._svgpcb_footprint_block_path_of(block_ref) port_path = footprint_path.append_port(*pin_ref) - candidate_nets = [net for net in self._svgpcb_netlist.nets - if port_path in net.ports] + candidate_nets = [net for net in self._svgpcb_netlist.nets if port_path in net.ports] assert len(candidate_nets) == 1 - candidate_pins = [pin for pin in candidate_nets[0].pins - if pin.block_path == footprint_path] + candidate_pins = [pin for pin in candidate_nets[0].pins if pin.block_path == footprint_path] assert len(candidate_pins) == 1 return candidate_pins[0].pin_name diff --git a/edg/electronics_model/TouchPort.py b/edg/electronics_model/TouchPort.py index 42e5c9327..d8e67dd17 100644 --- a/edg/electronics_model/TouchPort.py +++ b/edg/electronics_model/TouchPort.py @@ -4,22 +4,25 @@ class TouchLink(CircuitLink): - """Touch sensor link, consisting of one sensor (typically a PCB copper pattern) and one driver. - These contain no modeling.""" - def __init__(self) -> None: - super().__init__() - self.driver = self.Port(TouchDriver()) - self.pad = self.Port(TouchPadPort()) + """Touch sensor link, consisting of one sensor (typically a PCB copper pattern) and one driver. + These contain no modeling.""" + + def __init__(self) -> None: + super().__init__() + self.driver = self.Port(TouchDriver()) + self.pad = self.Port(TouchPadPort()) class TouchDriver(CircuitPort[TouchLink]): - """Touch sensor driver-side port, typically attached to a microcontroller pin. - Internal to this port should be any circuitry needed to make the sensor work. - Separately from the port, the microcontroller should generate additionally needed circuits, - like the sensing caps for STM32 devices.""" - link_type = TouchLink + """Touch sensor driver-side port, typically attached to a microcontroller pin. + Internal to this port should be any circuitry needed to make the sensor work. + Separately from the port, the microcontroller should generate additionally needed circuits, + like the sensing caps for STM32 devices.""" + + link_type = TouchLink class TouchPadPort(CircuitPort[TouchLink]): - """Touch sensor-side port, typically attached to a copper pad.""" - link_type = TouchLink + """Touch sensor-side port, typically attached to a copper pad.""" + + link_type = TouchLink diff --git a/edg/electronics_model/UartPort.py b/edg/electronics_model/UartPort.py index 686b730ef..87f87c364 100644 --- a/edg/electronics_model/UartPort.py +++ b/edg/electronics_model/UartPort.py @@ -7,28 +7,28 @@ class UartLink(Link): - def __init__(self) -> None: - super().__init__() - self.a = self.Port(UartPort(DigitalBidir.empty())) - self.b = self.Port(UartPort(DigitalBidir.empty())) + def __init__(self) -> None: + super().__init__() + self.a = self.Port(UartPort(DigitalBidir.empty())) + self.b = self.Port(UartPort(DigitalBidir.empty())) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.a_tx = self.connect(self.a.tx, self.b.rx) - self.b_tx = self.connect(self.b.tx, self.a.rx) + self.a_tx = self.connect(self.a.tx, self.b.rx) + self.b_tx = self.connect(self.b.tx, self.a.rx) class UartPort(Bundle[UartLink]): - link_type = UartLink + link_type = UartLink - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.tx = self.Port(DigitalSource.from_bidir(model)) - self.rx = self.Port(DigitalSink.from_bidir(model)) + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.tx = self.Port(DigitalSource.from_bidir(model)) + self.rx = self.Port(DigitalSink.from_bidir(model)) - self.baud = self.Parameter(RangeExpr(RangeExpr.ZERO)) - self.baud_limit = self.Parameter(RangeExpr(RangeExpr.INF)) + self.baud = self.Parameter(RangeExpr(RangeExpr.ZERO)) + self.baud_limit = self.Parameter(RangeExpr(RangeExpr.INF)) diff --git a/edg/electronics_model/Units.py b/edg/electronics_model/Units.py index 7e1cd43f7..eb31b8b11 100644 --- a/edg/electronics_model/Units.py +++ b/edg/electronics_model/Units.py @@ -2,82 +2,82 @@ import math -Farad = LiteralConstructor(1, 'F') -uFarad = LiteralConstructor(1e-6, 'F') -nFarad = LiteralConstructor(1e-9, 'F') -pFarad = LiteralConstructor(1e-12, 'F') +Farad = LiteralConstructor(1, "F") +uFarad = LiteralConstructor(1e-6, "F") +nFarad = LiteralConstructor(1e-9, "F") +pFarad = LiteralConstructor(1e-12, "F") -Amp = LiteralConstructor(1, 'A') -mAmp = LiteralConstructor(1e-3, 'A') -uAmp = LiteralConstructor(1e-6, 'A') -nAmp = LiteralConstructor(1e-9, 'A') -pAmp = LiteralConstructor(1e-12, 'A') +Amp = LiteralConstructor(1, "A") +mAmp = LiteralConstructor(1e-3, "A") +uAmp = LiteralConstructor(1e-6, "A") +nAmp = LiteralConstructor(1e-9, "A") +pAmp = LiteralConstructor(1e-12, "A") -Volt = LiteralConstructor(1, 'V') -mVolt = LiteralConstructor(1e-3, 'V') +Volt = LiteralConstructor(1, "V") +mVolt = LiteralConstructor(1e-3, "V") -MOhm = LiteralConstructor(1e6, 'Ω') -kOhm = LiteralConstructor(1e3, 'Ω') -Ohm = LiteralConstructor(1, 'Ω') -mOhm = LiteralConstructor(1e-3, 'Ω') +MOhm = LiteralConstructor(1e6, "Ω") +kOhm = LiteralConstructor(1e3, "Ω") +Ohm = LiteralConstructor(1, "Ω") +mOhm = LiteralConstructor(1e-3, "Ω") -Henry = LiteralConstructor(1, 'H') -mHenry = LiteralConstructor(1e-3, 'H') -uHenry = LiteralConstructor(1e-6, 'H') -nHenry = LiteralConstructor(1e-9, 'H') +Henry = LiteralConstructor(1, "H") +mHenry = LiteralConstructor(1e-3, "H") +uHenry = LiteralConstructor(1e-6, "H") +nHenry = LiteralConstructor(1e-9, "H") -Hertz = LiteralConstructor(1, 'Hz') -kHertz = LiteralConstructor(1e3, 'Hz') -MHertz = LiteralConstructor(1e6, 'Hz') -GHertz = LiteralConstructor(1e9, 'Hz') +Hertz = LiteralConstructor(1, "Hz") +kHertz = LiteralConstructor(1e3, "Hz") +MHertz = LiteralConstructor(1e6, "Hz") +GHertz = LiteralConstructor(1e9, "Hz") -Second = LiteralConstructor(1, 'S') -mSecond = LiteralConstructor(1e-3, 'S') -uSecond = LiteralConstructor(1e-6, 'S') -nSecond = LiteralConstructor(1e-9, 'S') +Second = LiteralConstructor(1, "S") +mSecond = LiteralConstructor(1e-3, "S") +uSecond = LiteralConstructor(1e-6, "S") +nSecond = LiteralConstructor(1e-9, "S") -Bit = LiteralConstructor(1, 'bit') -kiBit = LiteralConstructor(1024, 'bit') # Ki/Mi (kibi/mebi) means factor of 1024 instead of 1000 -MiBit = LiteralConstructor(1024*1024, 'bit') +Bit = LiteralConstructor(1, "bit") +kiBit = LiteralConstructor(1024, "bit") # Ki/Mi (kibi/mebi) means factor of 1024 instead of 1000 +MiBit = LiteralConstructor(1024 * 1024, "bit") -Watt = LiteralConstructor(1, 'W') +Watt = LiteralConstructor(1, "W") -Ratio = LiteralConstructor(1, '') +Ratio = LiteralConstructor(1, "") class UnitUtils: - PREFIXES_POW3_HIGH = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] - PREFIXES_POW3_LOW = ['m', 'u', 'n', 'p', 'f', 'a', 'z', 'y'] - - @classmethod - def num_to_prefix(cls, num: float, figs: int) -> str: - if num == 0: - return '0' - elif num == -float('inf'): - return '-inf' - elif num == float('inf'): - return 'inf' - elif num < 0: - sign = '-' - num = -num - else: - sign = '' - - num_pwr3 = math.floor(math.log10(num) / 3) - if num_pwr3 < 0: - prefix_set = cls.PREFIXES_POW3_LOW - num_pwr3_sign = -1 - else: - prefix_set = cls.PREFIXES_POW3_HIGH - num_pwr3_sign = 1 - num_pwr3 *= num_pwr3_sign - - if num_pwr3 > len(prefix_set) + 1: # clip to top number - num_pwr3 = len(prefix_set) + 1 - if num_pwr3 == 0: - prefix = '' - else: - prefix = prefix_set[num_pwr3 - 1] - - num_prefix = num * 10**(-1 * num_pwr3_sign * num_pwr3 * 3) - return f"{sign}{num_prefix:0.{figs}g}{prefix}" + PREFIXES_POW3_HIGH = ["k", "M", "G", "T", "P", "E", "Z", "Y"] + PREFIXES_POW3_LOW = ["m", "u", "n", "p", "f", "a", "z", "y"] + + @classmethod + def num_to_prefix(cls, num: float, figs: int) -> str: + if num == 0: + return "0" + elif num == -float("inf"): + return "-inf" + elif num == float("inf"): + return "inf" + elif num < 0: + sign = "-" + num = -num + else: + sign = "" + + num_pwr3 = math.floor(math.log10(num) / 3) + if num_pwr3 < 0: + prefix_set = cls.PREFIXES_POW3_LOW + num_pwr3_sign = -1 + else: + prefix_set = cls.PREFIXES_POW3_HIGH + num_pwr3_sign = 1 + num_pwr3 *= num_pwr3_sign + + if num_pwr3 > len(prefix_set) + 1: # clip to top number + num_pwr3 = len(prefix_set) + 1 + if num_pwr3 == 0: + prefix = "" + else: + prefix = prefix_set[num_pwr3 - 1] + + num_prefix = num * 10 ** (-1 * num_pwr3_sign * num_pwr3 * 3) + return f"{sign}{num_prefix:0.{figs}g}{prefix}" diff --git a/edg/electronics_model/UsbPort.py b/edg/electronics_model/UsbPort.py index 815c5087e..ba6bf15ec 100644 --- a/edg/electronics_model/UsbPort.py +++ b/edg/electronics_model/UsbPort.py @@ -7,95 +7,98 @@ class UsbLink(Link): - def __init__(self) -> None: - super().__init__() - self.host = self.Port(UsbHostPort()) - self.device = self.Port(UsbDevicePort()) - self.passive = self.Port(Vector(UsbPassivePort()), optional=True) + def __init__(self) -> None: + super().__init__() + self.host = self.Port(UsbHostPort()) + self.device = self.Port(UsbDevicePort()) + self.passive = self.Port(Vector(UsbPassivePort()), optional=True) - @override - def contents(self) -> None: - super().contents() - # TODO write protocol-level signal constraints? + @override + def contents(self) -> None: + super().contents() + # TODO write protocol-level signal constraints? - self.d_P = self.connect(self.host.dp, self.device.dp, self.passive.map_extract(lambda port: port.dp), - flatten=True) - self.d_N = self.connect(self.host.dm, self.device.dm, self.passive.map_extract(lambda port: port.dm), - flatten=True) + self.d_P = self.connect( + self.host.dp, self.device.dp, self.passive.map_extract(lambda port: port.dp), flatten=True + ) + self.d_N = self.connect( + self.host.dm, self.device.dm, self.passive.map_extract(lambda port: port.dm), flatten=True + ) class UsbHostPort(Bundle[UsbLink]): - link_type = UsbLink + link_type = UsbLink - def __init__(self) -> None: - super().__init__() - self.dp = self.Port(DigitalBidir()) - self.dm = self.Port(DigitalBidir()) + def __init__(self) -> None: + super().__init__() + self.dp = self.Port(DigitalBidir()) + self.dm = self.Port(DigitalBidir()) class UsbDeviceBridge(PortBridge): - def __init__(self) -> None: - super().__init__() - self.outer_port = self.Port(UsbDevicePort.empty()) - self.inner_link = self.Port(UsbHostPort.empty()) + def __init__(self) -> None: + super().__init__() + self.outer_port = self.Port(UsbDevicePort.empty()) + self.inner_link = self.Port(UsbHostPort.empty()) - @override - def contents(self) -> None: - from .DigitalPorts import DigitalBidirBridge - super().contents() + @override + def contents(self) -> None: + from .DigitalPorts import DigitalBidirBridge - self.dm_bridge = self.Block(DigitalBidirBridge()) - self.connect(self.outer_port.dm, self.dm_bridge.outer_port) - self.connect(self.dm_bridge.inner_link, self.inner_link.dm) + super().contents() - self.dp_bridge = self.Block(DigitalBidirBridge()) - self.connect(self.outer_port.dp, self.dp_bridge.outer_port) - self.connect(self.dp_bridge.inner_link, self.inner_link.dp) + self.dm_bridge = self.Block(DigitalBidirBridge()) + self.connect(self.outer_port.dm, self.dm_bridge.outer_port) + self.connect(self.dm_bridge.inner_link, self.inner_link.dm) + + self.dp_bridge = self.Block(DigitalBidirBridge()) + self.connect(self.outer_port.dp, self.dp_bridge.outer_port) + self.connect(self.dp_bridge.inner_link, self.inner_link.dp) class UsbDevicePort(Bundle[UsbLink]): - link_type = UsbLink - bridge_type = UsbDeviceBridge + link_type = UsbLink + bridge_type = UsbDeviceBridge - def __init__(self, model: Optional[DigitalBidir] = None) -> None: - super().__init__() - if model is None: - model = DigitalBidir() # ideal by default - self.dp = self.Port(model) - self.dm = self.Port(model) + def __init__(self, model: Optional[DigitalBidir] = None) -> None: + super().__init__() + if model is None: + model = DigitalBidir() # ideal by default + self.dp = self.Port(model) + self.dm = self.Port(model) class UsbPassivePort(Bundle[UsbLink]): - link_type = UsbLink + link_type = UsbLink - def __init__(self) -> None: - super().__init__() - self.dp = self.Port(DigitalBidir()) - self.dm = self.Port(DigitalBidir()) + def __init__(self) -> None: + super().__init__() + self.dp = self.Port(DigitalBidir()) + self.dm = self.Port(DigitalBidir()) class UsbCcLink(Link): - def __init__(self) -> None: - super().__init__() - # TODO should we have UFP/DFP/DRD support? - # TODO note that CC is pulled up on source (DFP) side - self.a = self.Port(UsbCcPort()) - self.b = self.Port(UsbCcPort()) - - @override - def contents(self) -> None: - super().contents() - # TODO perhaps enable crossover connections as optional layout optimization? - # TODO check both b and pull aren't simultaneously connected? - # TODO write protocol-level signal constraints? - self.cc1 = self.connect(self.a.cc1, self.b.cc1) - self.cc2 = self.connect(self.a.cc2, self.b.cc2) + def __init__(self) -> None: + super().__init__() + # TODO should we have UFP/DFP/DRD support? + # TODO note that CC is pulled up on source (DFP) side + self.a = self.Port(UsbCcPort()) + self.b = self.Port(UsbCcPort()) + + @override + def contents(self) -> None: + super().contents() + # TODO perhaps enable crossover connections as optional layout optimization? + # TODO check both b and pull aren't simultaneously connected? + # TODO write protocol-level signal constraints? + self.cc1 = self.connect(self.a.cc1, self.b.cc1) + self.cc2 = self.connect(self.a.cc2, self.b.cc2) class UsbCcPort(Bundle[UsbCcLink]): - link_type = UsbCcLink + link_type = UsbCcLink - def __init__(self, pullup_capable: BoolLike = False) -> None: - super().__init__() - self.cc1 = self.Port(DigitalBidir(pullup_capable=pullup_capable)) - self.cc2 = self.Port(DigitalBidir(pullup_capable=pullup_capable)) + def __init__(self, pullup_capable: BoolLike = False) -> None: + super().__init__() + self.cc1 = self.Port(DigitalBidir(pullup_capable=pullup_capable)) + self.cc2 = self.Port(DigitalBidir(pullup_capable=pullup_capable)) diff --git a/edg/electronics_model/VoltagePorts.py b/edg/electronics_model/VoltagePorts.py index fc4c2c178..c1a479eef 100644 --- a/edg/electronics_model/VoltagePorts.py +++ b/edg/electronics_model/VoltagePorts.py @@ -10,195 +10,194 @@ from .Units import Volt, Ohm, Amp if TYPE_CHECKING: - from .DigitalPorts import DigitalSource - from .AnalogPort import AnalogSource + from .DigitalPorts import DigitalSource + from .AnalogPort import AnalogSource class VoltageLink(CircuitLink): - @classmethod - def _voltage_range(cls, port: Port[VoltageLink]) -> RangeExpr: - """Returns the voltage for a Voltage port, either sink or source""" - if isinstance(port, VoltageSource): - return port.voltage_out - elif isinstance(port, VoltageSink): - return port.link().voltage - else: - raise TypeError - - @classmethod - def _supply_voltage_range(cls, neg: Port[GroundLink], pos: Port[VoltageLink]) -> RangeExpr: - """For a negative and positive Voltage port (either sink or source), returns the voltage span.""" - return GroundLink._voltage_range(neg).hull(cls._voltage_range(pos)) - - def __init__(self) -> None: - super().__init__() - - self.source = self.Port(VoltageSource()) - self.sinks = self.Port(Vector(VoltageSink())) - - self.voltage = self.Parameter(RangeExpr()) - self.voltage_limits = self.Parameter(RangeExpr()) - self.current_drawn = self.Parameter(RangeExpr()) - self.current_limits = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - super().contents() - - self.description = DescriptionString( - "voltage: ", DescriptionString.FormatUnits(self.voltage, "V"), - " of limits: ", DescriptionString.FormatUnits(self.voltage_limits, "V"), - "\ncurrent: ", DescriptionString.FormatUnits(self.current_drawn, "A"), - " of limits: ", DescriptionString.FormatUnits(self.current_limits, "A")) - - self.assign(self.voltage, self.source.voltage_out) - self.assign(self.voltage_limits, self.sinks.intersection(lambda x: x.voltage_limits)) - self.require(self.voltage_limits.contains(self.voltage), "overvoltage") - self.assign(self.current_limits, self.source.current_limits) - - self.assign(self.current_drawn, self.sinks.sum(lambda x: x.current_draw)) - self.require(self.current_limits.contains(self.current_drawn), "overcurrent") + @classmethod + def _voltage_range(cls, port: Port[VoltageLink]) -> RangeExpr: + """Returns the voltage for a Voltage port, either sink or source""" + if isinstance(port, VoltageSource): + return port.voltage_out + elif isinstance(port, VoltageSink): + return port.link().voltage + else: + raise TypeError + + @classmethod + def _supply_voltage_range(cls, neg: Port[GroundLink], pos: Port[VoltageLink]) -> RangeExpr: + """For a negative and positive Voltage port (either sink or source), returns the voltage span.""" + return GroundLink._voltage_range(neg).hull(cls._voltage_range(pos)) + + def __init__(self) -> None: + super().__init__() + + self.source = self.Port(VoltageSource()) + self.sinks = self.Port(Vector(VoltageSink())) + + self.voltage = self.Parameter(RangeExpr()) + self.voltage_limits = self.Parameter(RangeExpr()) + self.current_drawn = self.Parameter(RangeExpr()) + self.current_limits = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + super().contents() + + self.description = DescriptionString( + "voltage: ", + DescriptionString.FormatUnits(self.voltage, "V"), + " of limits: ", + DescriptionString.FormatUnits(self.voltage_limits, "V"), + "\ncurrent: ", + DescriptionString.FormatUnits(self.current_drawn, "A"), + " of limits: ", + DescriptionString.FormatUnits(self.current_limits, "A"), + ) + + self.assign(self.voltage, self.source.voltage_out) + self.assign(self.voltage_limits, self.sinks.intersection(lambda x: x.voltage_limits)) + self.require(self.voltage_limits.contains(self.voltage), "overvoltage") + self.assign(self.current_limits, self.source.current_limits) + + self.assign(self.current_drawn, self.sinks.sum(lambda x: x.current_draw)) + self.require(self.current_limits.contains(self.current_drawn), "overcurrent") class VoltageSinkBridge(CircuitPortBridge): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.outer_port = self.Port(VoltageSink(current_draw=RangeExpr(), - voltage_limits=RangeExpr())) + self.outer_port = self.Port(VoltageSink(current_draw=RangeExpr(), voltage_limits=RangeExpr())) - # Here we ignore the current_limits of the inner port, instead relying on the main link to handle it - # The outer port's voltage_limits is untouched and should be defined in the port def. - # TODO: it's a slightly optimization to handle them here. Should it be done? - # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? - self.inner_link = self.Port(VoltageSource(current_limits=RangeExpr.ALL, - voltage_out=RangeExpr())) + # Here we ignore the current_limits of the inner port, instead relying on the main link to handle it + # The outer port's voltage_limits is untouched and should be defined in the port def. + # TODO: it's a slightly optimization to handle them here. Should it be done? + # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? + self.inner_link = self.Port(VoltageSource(current_limits=RangeExpr.ALL, voltage_out=RangeExpr())) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.assign(self.outer_port.current_draw, self.inner_link.link().current_drawn) - self.assign(self.outer_port.voltage_limits, self.inner_link.link().voltage_limits) + self.assign(self.outer_port.current_draw, self.inner_link.link().current_drawn) + self.assign(self.outer_port.voltage_limits, self.inner_link.link().voltage_limits) - self.assign(self.inner_link.voltage_out, self.outer_port.link().voltage) + self.assign(self.inner_link.voltage_out, self.outer_port.link().voltage) class VoltageSourceBridge(CircuitPortBridge): # basic passthrough port, sources look the same inside and outside - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.outer_port = self.Port(VoltageSource(voltage_out=RangeExpr(), - current_limits=RangeExpr())) + self.outer_port = self.Port(VoltageSource(voltage_out=RangeExpr(), current_limits=RangeExpr())) - # Here we ignore the voltage_limits of the inner port, instead relying on the main link to handle it - # The outer port's current_limits is untouched and should be defined in tte port def. - # TODO: it's a slightly optimization to handle them here. Should it be done? - # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? - self.inner_link = self.Port(VoltageSink(voltage_limits=RangeExpr.ALL, - current_draw=RangeExpr())) + # Here we ignore the voltage_limits of the inner port, instead relying on the main link to handle it + # The outer port's current_limits is untouched and should be defined in tte port def. + # TODO: it's a slightly optimization to handle them here. Should it be done? + # TODO: or maybe current_limits / voltage_limits shouldn't be a port, but rather a block property? + self.inner_link = self.Port(VoltageSink(voltage_limits=RangeExpr.ALL, current_draw=RangeExpr())) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.assign(self.outer_port.voltage_out, self.inner_link.link().voltage) - self.assign(self.outer_port.current_limits, self.inner_link.link().current_limits) # TODO adjust for inner current drawn + self.assign(self.outer_port.voltage_out, self.inner_link.link().voltage) + self.assign( + self.outer_port.current_limits, self.inner_link.link().current_limits + ) # TODO adjust for inner current drawn - self.assign(self.inner_link.current_draw, self.outer_port.link().current_drawn) + self.assign(self.inner_link.current_draw, self.outer_port.link().current_drawn) class VoltageBase(CircuitPort[VoltageLink]): - link_type = VoltageLink + link_type = VoltageLink - # TODO: support isolation domains and offset grounds + # TODO: support isolation domains and offset grounds - # these are here (instead of in VoltageSource) since the port may be on the other side of a bridge - def as_ground(self, current_draw: RangeLike) -> GroundReference: - """Adapts this port to a ground. Current draw is the current drawn from this port, and is required - since ground does not model current draw. - """ - return self._convert(VoltageSinkAdapterGroundReference(current_draw)) + # these are here (instead of in VoltageSource) since the port may be on the other side of a bridge + def as_ground(self, current_draw: RangeLike) -> GroundReference: + """Adapts this port to a ground. Current draw is the current drawn from this port, and is required + since ground does not model current draw. + """ + return self._convert(VoltageSinkAdapterGroundReference(current_draw)) - def as_digital_source(self) -> DigitalSource: - return self._convert(VoltageSinkAdapterDigitalSource()) + def as_digital_source(self) -> DigitalSource: + return self._convert(VoltageSinkAdapterDigitalSource()) - def as_analog_source(self) -> AnalogSource: - return self._convert(VoltageSinkAdapterAnalogSource()) + def as_analog_source(self) -> AnalogSource: + return self._convert(VoltageSinkAdapterAnalogSource()) class VoltageSink(VoltageBase): - bridge_type = VoltageSinkBridge - - @staticmethod - def from_gnd(gnd: Port[GroundLink], voltage_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO) -> 'VoltageSink': - return VoltageSink( - voltage_limits=gnd.link().voltage + voltage_limits, - current_draw=current_draw - ) - - def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, - current_draw: RangeLike = RangeExpr.ZERO) -> None: - super().__init__() - self.voltage_limits: RangeExpr = self.Parameter(RangeExpr(voltage_limits)) - self.current_draw: RangeExpr = self.Parameter(RangeExpr(current_draw)) - - -class VoltageSinkAdapterGroundReference(CircuitPortAdapter['GroundReference']): - def __init__(self, current_draw: RangeLike): - super().__init__() - from .GroundPort import GroundReference - self.src = self.Port(VoltageSink( - voltage_limits=RangeExpr.ALL * Volt, - current_draw=current_draw - )) - self.dst = self.Port(GroundReference( - voltage_out=self.src.link().voltage, - )) - - -class VoltageSinkAdapterDigitalSource(CircuitPortAdapter['DigitalSource']): - def __init__(self) -> None: - from .DigitalPorts import DigitalSource - super().__init__() - self.src = self.Port(VoltageSink( - voltage_limits=RangeExpr.ALL * Volt, - current_draw=RangeExpr() - )) - self.dst = self.Port(DigitalSource( - voltage_out=self.src.link().voltage, - output_thresholds=(FloatExpr._to_expr_type(-float('inf')), self.src.link().voltage.upper()) - )) - self.assign(self.src.current_draw, self.dst.link().current_drawn) # TODO might be an overestimate - - -class VoltageSinkAdapterAnalogSource(CircuitPortAdapter['AnalogSource']): - def __init__(self) -> None: - from .AnalogPort import AnalogSource - - super().__init__() - self.src = self.Port(VoltageSink( - current_draw=RangeExpr() - )) - self.dst = self.Port(AnalogSource( - voltage_out=self.src.link().voltage, - signal_out=self.src.link().voltage, - impedance=(0, 0)*Ohm, # TODO not actually true, but pretty darn low? - )) - - # TODO might be an overestimate - self.assign(self.src.current_draw, self.dst.link().current_drawn) + bridge_type = VoltageSinkBridge + + @staticmethod + def from_gnd( + gnd: Port[GroundLink], voltage_limits: RangeLike = RangeExpr.ALL, current_draw: RangeLike = RangeExpr.ZERO + ) -> "VoltageSink": + return VoltageSink(voltage_limits=gnd.link().voltage + voltage_limits, current_draw=current_draw) + + def __init__(self, voltage_limits: RangeLike = RangeExpr.ALL, current_draw: RangeLike = RangeExpr.ZERO) -> None: + super().__init__() + self.voltage_limits: RangeExpr = self.Parameter(RangeExpr(voltage_limits)) + self.current_draw: RangeExpr = self.Parameter(RangeExpr(current_draw)) + + +class VoltageSinkAdapterGroundReference(CircuitPortAdapter["GroundReference"]): + def __init__(self, current_draw: RangeLike): + super().__init__() + from .GroundPort import GroundReference + + self.src = self.Port(VoltageSink(voltage_limits=RangeExpr.ALL * Volt, current_draw=current_draw)) + self.dst = self.Port( + GroundReference( + voltage_out=self.src.link().voltage, + ) + ) + + +class VoltageSinkAdapterDigitalSource(CircuitPortAdapter["DigitalSource"]): + def __init__(self) -> None: + from .DigitalPorts import DigitalSource + + super().__init__() + self.src = self.Port(VoltageSink(voltage_limits=RangeExpr.ALL * Volt, current_draw=RangeExpr())) + self.dst = self.Port( + DigitalSource( + voltage_out=self.src.link().voltage, + output_thresholds=(FloatExpr._to_expr_type(-float("inf")), self.src.link().voltage.upper()), + ) + ) + self.assign(self.src.current_draw, self.dst.link().current_drawn) # TODO might be an overestimate + + +class VoltageSinkAdapterAnalogSource(CircuitPortAdapter["AnalogSource"]): + def __init__(self) -> None: + from .AnalogPort import AnalogSource + + super().__init__() + self.src = self.Port(VoltageSink(current_draw=RangeExpr())) + self.dst = self.Port( + AnalogSource( + voltage_out=self.src.link().voltage, + signal_out=self.src.link().voltage, + impedance=(0, 0) * Ohm, # TODO not actually true, but pretty darn low? + ) + ) + + # TODO might be an overestimate + self.assign(self.src.current_draw, self.dst.link().current_drawn) class VoltageSource(VoltageBase): - bridge_type = VoltageSourceBridge + bridge_type = VoltageSourceBridge - def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, - current_limits: RangeLike = RangeExpr.ALL) -> None: - super().__init__() - self.voltage_out: RangeExpr = self.Parameter(RangeExpr(voltage_out)) - self.current_limits: RangeExpr = self.Parameter(RangeExpr(current_limits)) + def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO, current_limits: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.voltage_out: RangeExpr = self.Parameter(RangeExpr(voltage_out)) + self.current_limits: RangeExpr = self.Parameter(RangeExpr(current_limits)) Power = PortTag(VoltageSink) # General positive voltage port, should only be mutually exclusive with the below diff --git a/edg/electronics_model/__init__.py b/edg/electronics_model/__init__.py index bf233b3e7..acdb0fdaa 100644 --- a/edg/electronics_model/__init__.py +++ b/edg/electronics_model/__init__.py @@ -39,6 +39,7 @@ from .KiCadImportableBlock import KiCadImportableBlock, KiCadInstantiableBlock from .KiCadSchematicBlock import KiCadSchematicBlock + # for power users to build custom blackbox handlers from .KiCadSchematicParser import KiCadSymbol, KiCadLibSymbol from .KiCadSchematicBlock import KiCadBlackbox, KiCadBlackboxBase diff --git a/edg/electronics_model/footprint.py b/edg/electronics_model/footprint.py index 589f6998c..f17c32f57 100644 --- a/edg/electronics_model/footprint.py +++ b/edg/electronics_model/footprint.py @@ -12,133 +12,168 @@ class RefdesMode(Enum): Pathname = auto() # pathname as refdes, value passed through - ############################################################################################################################################################################################### """1. Generating Header""" + def gen_header() -> str: - return '(export (version D)' + return "(export (version D)" + ############################################################################################################################################################################################### """2. Generating Blocks""" + def block_name(block: NetBlock, refdes_mode: RefdesMode) -> str: if refdes_mode == RefdesMode.Pathname: - return '.'.join(block.path) + return ".".join(block.path) else: return block.refdes # default is conventional refdes + def gen_block_comp(block_name: str) -> str: return f'(comp (ref "{block_name}")' + def gen_block_value(block: NetBlock, refdes_mode: RefdesMode) -> str: if refdes_mode == RefdesMode.PathnameAsValue: - if 'TP' in block.refdes: # test points keep their value + if "TP" in block.refdes: # test points keep their value return f'(value "{block.value}")' else: - pathname = '.'.join(block.path) + pathname = ".".join(block.path) return f'(value "{pathname}")' else: return f'(value "{block.value}")' + def gen_block_footprint(block_footprint: str) -> str: return f'(footprint "{block_footprint}")' + def gen_block_tstamp(block_path: List[str]) -> str: blockpath_hash = f"{zlib.adler32(str.encode(block_path[-1])):08x}" return f'(tstamps "{blockpath_hash}"))' + def gen_block_sheetpath(sheetpath: List[str]) -> str: - sheetpath_hash = [f"{zlib.adler32(str.encode(sheetpath_elt)):08x}" for sheetpath_elt in sheetpath] - sheetpath_str = '/' + '/'.join(sheetpath) - sheetpath_hash_str = '/' + '/'.join(sheetpath_hash) - if sheetpath: # need to add trailing / - sheetpath_str = sheetpath_str + '/' - sheetpath_hash_str = sheetpath_hash_str + '/' - return f'(sheetpath (names "{sheetpath_str}") (tstamps "{sheetpath_hash_str}"))' + sheetpath_hash = [f"{zlib.adler32(str.encode(sheetpath_elt)):08x}" for sheetpath_elt in sheetpath] + sheetpath_str = "/" + "/".join(sheetpath) + sheetpath_hash_str = "/" + "/".join(sheetpath_hash) + if sheetpath: # need to add trailing / + sheetpath_str = sheetpath_str + "/" + sheetpath_hash_str = sheetpath_hash_str + "/" + return f'(sheetpath (names "{sheetpath_str}") (tstamps "{sheetpath_hash_str}"))' + def gen_block_prop_sheetname(block_path: List[str]) -> str: - if len(block_path) >= 2: - value = block_path[-2] - else: - value = "" - return f'(property (name "Sheetname") (value "{value}"))' + if len(block_path) >= 2: + value = block_path[-2] + else: + value = "" + return f'(property (name "Sheetname") (value "{value}"))' + def gen_block_prop_sheetfile(block_path: List[edgir.LibraryPath]) -> str: - if len(block_path) >= 2: - value = block_path[-2].target.name - else: - value = "" - return f'(property (name "Sheetfile") (value "{value}"))' + if len(block_path) >= 2: + value = block_path[-2].target.name + else: + value = "" + return f'(property (name "Sheetfile") (value "{value}"))' + def gen_block_prop_edg(block: NetBlock) -> str: - return f'(property (name "edg_path") (value "{".".join(block.full_path.to_tuple())}"))\n' +\ - f' (property (name "edg_short_path") (value "{".".join(block.path)}"))\n' + \ - f' (property (name "edg_refdes") (value "{block.refdes}"))\n' + \ - f' (property (name "edg_part") (value "{block.part}"))\n' + \ - f' (property (name "edg_value") (value "{block.value}"))' + return ( + f'(property (name "edg_path") (value "{".".join(block.full_path.to_tuple())}"))\n' + + f' (property (name "edg_short_path") (value "{".".join(block.path)}"))\n' + + f' (property (name "edg_refdes") (value "{block.refdes}"))\n' + + f' (property (name "edg_part") (value "{block.part}"))\n' + + f' (property (name "edg_value") (value "{block.value}"))' + ) + def block_exp(blocks: List[NetBlock], refdes_mode: RefdesMode) -> str: - """Given a dictionary of block_names (strings) as keys and Blocks (namedtuples) as corresponding values - - Example: - (components - (comp (ref U1) - (value LM555) - (footprint Package_DIP:DIP-4_W7.62mm) - (tstamp U1)) - (comp (ref R3) - (value 1k) - (footprint OptoDevice:R_LDR_4.9x4.2mm_P2.54mm_Vertical) - (tstamp R3)) - """ - result = '(components' - for block in blocks: - result += '\n' + gen_block_comp(block_name(block, refdes_mode)) + '\n' +\ - " " + gen_block_value(block, refdes_mode) + '\n' + \ - " " + gen_block_footprint(block.footprint) + '\n' + \ - " " + gen_block_prop_sheetname(block.path) + '\n' + \ - " " + gen_block_prop_sheetfile(block.class_path) + '\n' + \ - " " + gen_block_prop_edg(block) + '\n' + \ - " " + gen_block_sheetpath(block.path[:-1]) + '\n' + \ - " " + gen_block_tstamp(block.path) - return result + ')' + """Given a dictionary of block_names (strings) as keys and Blocks (namedtuples) as corresponding values + + Example: + (components + (comp (ref U1) + (value LM555) + (footprint Package_DIP:DIP-4_W7.62mm) + (tstamp U1)) + (comp (ref R3) + (value 1k) + (footprint OptoDevice:R_LDR_4.9x4.2mm_P2.54mm_Vertical) + (tstamp R3)) + """ + result = "(components" + for block in blocks: + result += ( + "\n" + + gen_block_comp(block_name(block, refdes_mode)) + + "\n" + + " " + + gen_block_value(block, refdes_mode) + + "\n" + + " " + + gen_block_footprint(block.footprint) + + "\n" + + " " + + gen_block_prop_sheetname(block.path) + + "\n" + + " " + + gen_block_prop_sheetfile(block.class_path) + + "\n" + + " " + + gen_block_prop_edg(block) + + "\n" + + " " + + gen_block_sheetpath(block.path[:-1]) + + "\n" + + " " + + gen_block_tstamp(block.path) + ) + return result + ")" + ############################################################################################################################################################################################### """3. Generating Nets""" + def gen_net_header(net_count: int, net_name: str) -> str: return '(net (code {}) (name "{}")'.format(net_count, net_name) + def gen_net_pin(block_name: str, pin_name: str) -> str: return "(node (ref {}) (pin {}))".format(block_name, pin_name) + def net_exp(nets: List[Net], blocks: List[NetBlock], refdes_mode: RefdesMode) -> str: - """Given a dictionary of net names (strings) as keys and a list of connected Pins (namedtuples) as corresponding values - - Example: - (nets - (net (code 1) (name "Net-(R1-Pad2)") - (node (ref R2) (pin 1)) - (node (ref R1) (pin 2))) - (net (code 3) (name GND) - (node (ref C2) (pin 2)) - (node (ref C1) (pin 2)) - (node (ref R4) (pin 2))) - - """ - block_dict = {block.full_path: block for block in blocks} - - result = '(nets' - for i, net in enumerate(nets): - result += '\n' + gen_net_header(i + 1, net.name) - for pin in net.pins: - result += '\n ' + gen_net_pin(block_name(block_dict[pin.block_path], refdes_mode), pin.pin_name) - result += ')' - return result + ')' + """Given a dictionary of net names (strings) as keys and a list of connected Pins (namedtuples) as corresponding values + + Example: + (nets + (net (code 1) (name "Net-(R1-Pad2)") + (node (ref R2) (pin 1)) + (node (ref R1) (pin 2))) + (net (code 3) (name GND) + (node (ref C2) (pin 2)) + (node (ref C1) (pin 2)) + (node (ref R4) (pin 2))) + + """ + block_dict = {block.full_path: block for block in blocks} + + result = "(nets" + for i, net in enumerate(nets): + result += "\n" + gen_net_header(i + 1, net.name) + for pin in net.pins: + result += "\n " + gen_net_pin(block_name(block_dict[pin.block_path], refdes_mode), pin.pin_name) + result += ")" + return result + ")" + ############################################################################################################################################################################################### @@ -146,5 +181,12 @@ def net_exp(nets: List[Net], blocks: List[NetBlock], refdes_mode: RefdesMode) -> def generate_netlist(netlist: Netlist, refdes_mode: RefdesMode) -> str: - return gen_header() + '\n' + block_exp(netlist.blocks, refdes_mode) + '\n' \ - + net_exp(netlist.nets, netlist.blocks, refdes_mode) + '\n' + ')' + return ( + gen_header() + + "\n" + + block_exp(netlist.blocks, refdes_mode) + + "\n" + + net_exp(netlist.nets, netlist.blocks, refdes_mode) + + "\n" + + ")" + ) diff --git a/edg/electronics_model/resources/build_kicad_footprint_table.py b/edg/electronics_model/resources/build_kicad_footprint_table.py index acbce4942..66c6ae17d 100644 --- a/edg/electronics_model/resources/build_kicad_footprint_table.py +++ b/edg/electronics_model/resources/build_kicad_footprint_table.py @@ -2,6 +2,7 @@ Utility script that crawls a local KiCad installation's library files and builds the table of footprint area. This file is pregenerated and committed to the repository, and used by the parts tables. """ + import os from typing import Dict, List, Tuple, Any, Optional import sexpdata # type: ignore @@ -9,7 +10,6 @@ KICAD_FP_DIRECTORIES = [ # "C:/Program Files/KiCad/8.0/share/kicad/footprints", - # for net-weaver-compiler os.path.join(os.path.dirname(__file__), "..", "..", "..", "examples"), os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "footprints", "kicad-footprints"), @@ -67,28 +67,29 @@ def sexp_list_find_all(container: list[Any], key: str) -> List[List[Any]]: """Given a sexp list, return all elements which are lists where the first element is a symbol of key.""" matching_elts = [] for elt in container: - if isinstance(elt, list) and elt and \ - isinstance(elt[0], sexpdata.Symbol) and elt[0].value() == key: + if isinstance(elt, list) and elt and isinstance(elt[0], sexpdata.Symbol) and elt[0].value() == key: matching_elts.append(elt) return matching_elts def calculate_area(fp_contents: str) -> Optional[float]: fp_top: List[Any] = sexpdata.loads(fp_contents) - assert isinstance(fp_top[0], sexpdata.Symbol) and fp_top[0].value() in ('footprint', 'module') + assert isinstance(fp_top[0], sexpdata.Symbol) and fp_top[0].value() in ("footprint", "module") # gather all expressions with type fp_line and layer F.CrtYd, and parse XYs fp_lines: List[Line] = [] - for fp_line_elt in sexp_list_find_all(fp_top, 'fp_line'): - layers = sexp_list_find_all(fp_line_elt, 'layer') + for fp_line_elt in sexp_list_find_all(fp_top, "fp_line"): + layers = sexp_list_find_all(fp_line_elt, "layer") assert len(layers) == 1 - if not (layers[0][1] == 'F.CrtYd' or (isinstance(layers[0][1], sexpdata.Symbol) and layers[0][1].value() == 'F.CrtYd')): + if not ( + layers[0][1] == "F.CrtYd" + or (isinstance(layers[0][1], sexpdata.Symbol) and layers[0][1].value() == "F.CrtYd") + ): continue - start = sexp_list_find_all(fp_line_elt, 'start') - end = sexp_list_find_all(fp_line_elt, 'end') + start = sexp_list_find_all(fp_line_elt, "start") + end = sexp_list_find_all(fp_line_elt, "end") assert len(start) == 1 and len(end) == 1 and len(start[0]) == 3 and len(end[0]) == 3 - fp_lines.append(((float(start[0][1]), float(start[0][2])), - (float(end[0][1]), float(end[0][2])))) + fp_lines.append(((float(start[0][1]), float(start[0][2])), (float(end[0][1]), float(end[0][2])))) area_opt = polygon_lines_area(fp_lines) if area_opt is not None: @@ -98,22 +99,22 @@ def calculate_area(fp_contents: str) -> Optional[float]: def calculate_bbox(fp_contents: str) -> Optional[Tuple[float, float, float, float]]: # x_min, y_min, x_max, y_max fp_top: List[Any] = sexpdata.loads(fp_contents) - assert isinstance(fp_top[0], sexpdata.Symbol) and fp_top[0].value() in ('footprint', 'module') + assert isinstance(fp_top[0], sexpdata.Symbol) and fp_top[0].value() in ("footprint", "module") - min_x = min_y = float('inf') - max_x = max_y = float('-inf') + min_x = min_y = float("inf") + max_x = max_y = float("-inf") # gather all expressions with type fp_line and layer F.CrtYd, and parse XYs - for fp_line_elt in sexp_list_find_all(fp_top, 'fp_line'): - start = sexp_list_find_all(fp_line_elt, 'start') - end = sexp_list_find_all(fp_line_elt, 'end') + for fp_line_elt in sexp_list_find_all(fp_top, "fp_line"): + start = sexp_list_find_all(fp_line_elt, "start") + end = sexp_list_find_all(fp_line_elt, "end") assert len(start) == 1 and len(end) == 1 and len(start[0]) == 3 and len(end[0]) == 3 min_x = min(min_x, float(start[0][1]), float(end[0][1])) max_x = max(max_x, float(start[0][1]), float(end[0][1])) min_y = min(min_y, float(start[0][2]), float(end[0][2])) max_y = max(max_y, float(start[0][2]), float(end[0][2])) - if min_x == float('inf') or min_y == float('inf') or max_x == float('-inf') or max_y == float('-inf'): + if min_x == float("inf") or min_y == float("inf") or max_x == float("-inf") or max_y == float("-inf"): return None return min_x, min_y, max_x, max_y @@ -131,13 +132,13 @@ class FootprintJson(RootModel[Dict[str, FootprintData]]): # script relpath impo fp_data_dict: Dict[str, FootprintData] = {} for kicad_dir in KICAD_FP_DIRECTORIES: for subdir in os.listdir(kicad_dir): - if not subdir.endswith('.pretty') or not os.path.isdir(os.path.join(kicad_dir, subdir)): + if not subdir.endswith(".pretty") or not os.path.isdir(os.path.join(kicad_dir, subdir)): continue - lib_name = subdir.split('.pretty')[0] + lib_name = subdir.split(".pretty")[0] for file in os.listdir(os.path.join(kicad_dir, subdir)): - if not file.endswith('.kicad_mod'): + if not file.endswith(".kicad_mod"): continue - fp_filename = file.split('.kicad_mod')[0] + fp_filename = file.split(".kicad_mod")[0] with open(os.path.join(kicad_dir, subdir, file)) as f: fp_data = f.read() @@ -148,13 +149,10 @@ class FootprintJson(RootModel[Dict[str, FootprintData]]): # script relpath impo fp_name = lib_name + ":" + fp_filename if fp_area is not None and fp_bbox is not None: - fp_data_dict[fp_name] = FootprintData( - area=fp_area, - bbox=fp_bbox - ) + fp_data_dict[fp_name] = FootprintData(area=fp_area, bbox=fp_bbox) print(f" {fp_name} -> {fp_area:.3g}, {fp_bbox}") else: print(f"skip {fp_name} {fp_area} {fp_bbox}") json = FootprintJson(fp_data_dict) - with open(OUTPUT_FILE, 'w') as f: + with open(OUTPUT_FILE, "w") as f: f.write(json.model_dump_json(indent=2)) diff --git a/edg/electronics_model/test_analog_link.py b/edg/electronics_model/test_analog_link.py index 73cb304c4..dcc737122 100644 --- a/edg/electronics_model/test_analog_link.py +++ b/edg/electronics_model/test_analog_link.py @@ -4,66 +4,67 @@ class AnalogSourceBlock(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(AnalogSource()) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(AnalogSource()) class AnalogSinkInfiniteBlock(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(AnalogSink( - impedance=RangeExpr.INF, - )) + def __init__(self) -> None: + super().__init__() + self.port = self.Port( + AnalogSink( + impedance=RangeExpr.INF, + ) + ) class AnalogSinkOneOhmBlock(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(AnalogSink( - impedance=(1, 1)*Ohm, - )) + def __init__(self) -> None: + super().__init__() + self.port = self.Port( + AnalogSink( + impedance=(1, 1) * Ohm, + ) + ) class AnalogTwoInfiniteTest(Block): - def __init__(self) -> None: - super().__init__() - self.source = self.Block(AnalogSourceBlock()) - self.sink1 = self.Block(AnalogSinkInfiniteBlock()) - self.sink2 = self.Block(AnalogSinkInfiniteBlock()) - self.link = self.connect(self.source.port, self.sink1.port, self.sink2.port) + def __init__(self) -> None: + super().__init__() + self.source = self.Block(AnalogSourceBlock()) + self.sink1 = self.Block(AnalogSinkInfiniteBlock()) + self.sink2 = self.Block(AnalogSinkInfiniteBlock()) + self.link = self.connect(self.source.port, self.sink1.port, self.sink2.port) class AnalogTwoOneOhmTest(Block): - def __init__(self) -> None: - super().__init__() - self.source = self.Block(AnalogSourceBlock()) - self.sink1 = self.Block(AnalogSinkOneOhmBlock()) - self.sink2 = self.Block(AnalogSinkOneOhmBlock()) - self.link = self.connect(self.source.port, self.sink1.port, self.sink2.port) + def __init__(self) -> None: + super().__init__() + self.source = self.Block(AnalogSourceBlock()) + self.sink1 = self.Block(AnalogSinkOneOhmBlock()) + self.sink2 = self.Block(AnalogSinkOneOhmBlock()) + self.link = self.connect(self.source.port, self.sink1.port, self.sink2.port) class AnalogMixedTest(Block): - def __init__(self) -> None: - super().__init__() - self.source = self.Block(AnalogSourceBlock()) - self.sink1 = self.Block(AnalogSinkInfiniteBlock()) - self.sink2 = self.Block(AnalogSinkOneOhmBlock()) - self.link = self.connect(self.source.port, self.sink1.port, self.sink2.port) + def __init__(self) -> None: + super().__init__() + self.source = self.Block(AnalogSourceBlock()) + self.sink1 = self.Block(AnalogSinkInfiniteBlock()) + self.sink2 = self.Block(AnalogSinkOneOhmBlock()) + self.link = self.connect(self.source.port, self.sink1.port, self.sink2.port) class AnalogLinkTestCase(unittest.TestCase): - def test_analog_two_infinite(self) -> None: - compiled = ScalaCompiler.compile(AnalogTwoInfiniteTest) - self.assertEqual(compiled.get_value(['link', 'sink_impedance']), - Range(float('inf'), float('inf'))) - - def test_analog_two_one_ohm(self) -> None: - compiled = ScalaCompiler.compile(AnalogTwoOneOhmTest) - self.assertEqual(compiled.get_value(['link', 'sink_impedance']), - Range(0.5, 0.5)) - - def test_analog_mixed(self) -> None: - compiled = ScalaCompiler.compile(AnalogMixedTest) - self.assertEqual(compiled.get_value(['link', 'sink_impedance']), - Range(1.0, 1.0)) + def test_analog_two_infinite(self) -> None: + compiled = ScalaCompiler.compile(AnalogTwoInfiniteTest) + self.assertEqual(compiled.get_value(["link", "sink_impedance"]), Range(float("inf"), float("inf"))) + + def test_analog_two_one_ohm(self) -> None: + compiled = ScalaCompiler.compile(AnalogTwoOneOhmTest) + self.assertEqual(compiled.get_value(["link", "sink_impedance"]), Range(0.5, 0.5)) + + def test_analog_mixed(self) -> None: + compiled = ScalaCompiler.compile(AnalogMixedTest) + self.assertEqual(compiled.get_value(["link", "sink_impedance"]), Range(1.0, 1.0)) diff --git a/edg/electronics_model/test_bundle_netlist.py b/edg/electronics_model/test_bundle_netlist.py index 5dad96c43..1f7919708 100644 --- a/edg/electronics_model/test_bundle_netlist.py +++ b/edg/electronics_model/test_bundle_netlist.py @@ -13,236 +13,342 @@ class TestFakeSpiController(FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.spi = self.Port(SpiController()) - self.cs_out_1 = self.Port(DigitalSource()) - self.cs_out_2 = self.Port(DigitalSource()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( # it's anyone's guess why the resistor array is a SPI controller - 'R', 'Resistor_SMD:R_Array_Concave_2x0603', - { - '0': self.cs_out_1, # the mythical and elusive pin 0 - '1': self.cs_out_2, - '2': self.spi.sck, - '3': self.spi.miso, - '4': self.spi.mosi, - }, - value='WeirdSpiController' - ) + def __init__(self) -> None: + super().__init__() + + self.spi = self.Port(SpiController()) + self.cs_out_1 = self.Port(DigitalSource()) + self.cs_out_2 = self.Port(DigitalSource()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( # it's anyone's guess why the resistor array is a SPI controller + "R", + "Resistor_SMD:R_Array_Concave_2x0603", + { + "0": self.cs_out_1, # the mythical and elusive pin 0 + "1": self.cs_out_2, + "2": self.spi.sck, + "3": self.spi.miso, + "4": self.spi.mosi, + }, + value="WeirdSpiController", + ) class TestFakeSpiPeripheral(FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.spi = self.Port(SpiPeripheral()) - self.cs_in = self.Port(DigitalSink()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( # it's anyone's guess why this resistor array has a different pinning in peripheral mode - 'R', 'Resistor_SMD:R_Array_Concave_2x0603', - { - '1': self.spi.sck, - '2': self.spi.mosi, - '3': self.spi.miso, - '4': self.cs_in, - }, - value='WeirdSpiPeripheral' - ) + def __init__(self) -> None: + super().__init__() + + self.spi = self.Port(SpiPeripheral()) + self.cs_in = self.Port(DigitalSink()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( # it's anyone's guess why this resistor array has a different pinning in peripheral mode + "R", + "Resistor_SMD:R_Array_Concave_2x0603", + { + "1": self.spi.sck, + "2": self.spi.mosi, + "3": self.spi.miso, + "4": self.cs_in, + }, + value="WeirdSpiPeripheral", + ) class TestSpiCircuit(DesignTop): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.controller = self.Block(TestFakeSpiController()) - self.peripheral1 = self.Block(TestFakeSpiPeripheral()) - self.peripheral2 = self.Block(TestFakeSpiPeripheral()) + self.controller = self.Block(TestFakeSpiController()) + self.peripheral1 = self.Block(TestFakeSpiPeripheral()) + self.peripheral2 = self.Block(TestFakeSpiPeripheral()) - self.spi_link = self.connect(self.controller.spi, self.peripheral1.spi, self.peripheral2.spi) - self.cs1_link = self.connect(self.controller.cs_out_1, self.peripheral1.cs_in) - self.cs2_link = self.connect(self.controller.cs_out_2, self.peripheral2.cs_in) + self.spi_link = self.connect(self.controller.spi, self.peripheral1.spi, self.peripheral2.spi) + self.cs1_link = self.connect(self.controller.cs_out_1, self.peripheral1.cs_in) + self.cs2_link = self.connect(self.controller.cs_out_2, self.peripheral2.cs_in) class TestFakeUartBlock(FootprintBlock): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.port = self.Port(UartPort()) + self.port = self.Port(UartPort()) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'R', 'Resistor_SMD:R_0603_1608Metric', - { - '1': self.port.tx, - '2': self.port.rx - }, - value='1k' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint("R", "Resistor_SMD:R_0603_1608Metric", {"1": self.port.tx, "2": self.port.rx}, value="1k") class TestUartCircuit(DesignTop): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.a = self.Block(TestFakeUartBlock()) - self.b = self.Block(TestFakeUartBlock()) + self.a = self.Block(TestFakeUartBlock()) + self.b = self.Block(TestFakeUartBlock()) - self.link = self.connect(self.a.port, self.b.port) + self.link = self.connect(self.a.port, self.b.port) class TestFakeCanBlock(FootprintBlock): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.port = self.Port(CanDiffPort()) + self.port = self.Port(CanDiffPort()) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'R', 'Resistor_SMD:R_0603_1608Metric', - { - '1': self.port.canh, - '2': self.port.canl - }, - value='120' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint("R", "Resistor_SMD:R_0603_1608Metric", {"1": self.port.canh, "2": self.port.canl}, value="120") class TestCanCircuit(DesignTop): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.node1 = self.Block(TestFakeCanBlock()) - self.node2 = self.Block(TestFakeCanBlock()) - self.node3 = self.Block(TestFakeCanBlock()) + self.node1 = self.Block(TestFakeCanBlock()) + self.node2 = self.Block(TestFakeCanBlock()) + self.node3 = self.Block(TestFakeCanBlock()) - self.link = self.connect(self.node1.port, self.node2.port, self.node3.port) + self.link = self.connect(self.node1.port, self.node2.port, self.node3.port) class BundleNetlistTestCase(unittest.TestCase): - def test_spi_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestSpiCircuit) - - self.assertIn(Net('cs1_link', [ - NetPin(['controller'], '0'), - NetPin(['peripheral1'], '4'), - ], [ - TransformUtil.Path.empty().append_block('controller').append_port('cs_out_1'), - TransformUtil.Path.empty().append_block('peripheral1').append_port('cs_in'), - ]), net.nets) - self.assertIn(Net('cs2_link', [ - NetPin(['controller'], '1'), - NetPin(['peripheral2'], '4'), - ], [ - TransformUtil.Path.empty().append_block('controller').append_port('cs_out_2'), - TransformUtil.Path.empty().append_block('peripheral2').append_port('cs_in'), - ]), net.nets) - self.assertIn(Net('spi_link.sck', [ - NetPin(['controller'], '2'), - NetPin(['peripheral1'], '1'), - NetPin(['peripheral2'], '1'), - ], [ - TransformUtil.Path.empty().append_block('controller').append_port('spi', 'sck'), - TransformUtil.Path.empty().append_block('peripheral1').append_port('spi', 'sck'), - TransformUtil.Path.empty().append_block('peripheral2').append_port('spi', 'sck'), - ]), net.nets) - self.assertIn(Net('spi_link.mosi', [ - NetPin(['controller'], '4'), - NetPin(['peripheral1'], '2'), - NetPin(['peripheral2'], '2'), - ], [ - TransformUtil.Path.empty().append_block('controller').append_port('spi', 'mosi'), - TransformUtil.Path.empty().append_block('peripheral1').append_port('spi', 'mosi'), - TransformUtil.Path.empty().append_block('peripheral2').append_port('spi', 'mosi'), - ]), net.nets) - self.assertIn(Net('spi_link.miso', [ - NetPin(['controller'], '3'), - NetPin(['peripheral1'], '3'), - NetPin(['peripheral2'], '3'), - ], [ - TransformUtil.Path.empty().append_block('controller').append_port('spi', 'miso'), - TransformUtil.Path.empty().append_block('peripheral1').append_port('spi', 'miso'), - TransformUtil.Path.empty().append_block('peripheral2').append_port('spi', 'miso'), - ]), net.nets) - - self.assertIn(NetBlock('Resistor_SMD:R_Array_Concave_2x0603', 'R1', '', 'WeirdSpiController', - ['controller'], ['controller'], - ['edg.electronics_model.test_bundle_netlist.TestFakeSpiController']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_Array_Concave_2x0603', 'R2', '', 'WeirdSpiPeripheral', - ['peripheral1'], ['peripheral1'], - ['edg.electronics_model.test_bundle_netlist.TestFakeSpiPeripheral']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_Array_Concave_2x0603', 'R3', '', 'WeirdSpiPeripheral', - ['peripheral2'], ['peripheral2'], - ['edg.electronics_model.test_bundle_netlist.TestFakeSpiPeripheral']), - net.blocks) - - def test_uart_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestUartCircuit) - - self.assertIn(Net('link.a_tx', [ - NetPin(['a'], '1'), - NetPin(['b'], '2') - ], [ - TransformUtil.Path.empty().append_block('a').append_port('port', 'tx'), - TransformUtil.Path.empty().append_block('b').append_port('port', 'rx'), - ]), net.nets) - self.assertIn(Net('link.b_tx', [ - NetPin(['a'], '2'), - NetPin(['b'], '1') - ], [ - TransformUtil.Path.empty().append_block('a').append_port('port', 'rx'), - TransformUtil.Path.empty().append_block('b').append_port('port', 'tx'), - ]), net.nets) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '1k', - ['a'], ['a'], ['edg.electronics_model.test_bundle_netlist.TestFakeUartBlock']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R2', '', '1k', - ['b'], ['b'], ['edg.electronics_model.test_bundle_netlist.TestFakeUartBlock']), - net.blocks) - - def test_can_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestCanCircuit) - - self.assertIn(Net('link.canh', [ - NetPin(['node1'], '1'), - NetPin(['node2'], '1'), - NetPin(['node3'], '1') - ], [ - TransformUtil.Path.empty().append_block('node1').append_port('port', 'canh'), - TransformUtil.Path.empty().append_block('node2').append_port('port', 'canh'), - TransformUtil.Path.empty().append_block('node3').append_port('port', 'canh'), - ]), net.nets) - self.assertIn(Net('link.canl', [ - NetPin(['node1'], '2'), - NetPin(['node2'], '2'), - NetPin(['node3'], '2') - ], [ - TransformUtil.Path.empty().append_block('node1').append_port('port', 'canl'), - TransformUtil.Path.empty().append_block('node2').append_port('port', 'canl'), - TransformUtil.Path.empty().append_block('node3').append_port('port', 'canl'), - ]), net.nets) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '120', - ['node1'], ['node1'], ['edg.electronics_model.test_bundle_netlist.TestFakeCanBlock']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R2', '', '120', - ['node2'], ['node2'], ['edg.electronics_model.test_bundle_netlist.TestFakeCanBlock']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R3', '', '120', - ['node3'], ['node3'], ['edg.electronics_model.test_bundle_netlist.TestFakeCanBlock']), - net.blocks) + def test_spi_netlist(self) -> None: + net = NetlistTestCase.generate_net(TestSpiCircuit) + + self.assertIn( + Net( + "cs1_link", + [ + NetPin(["controller"], "0"), + NetPin(["peripheral1"], "4"), + ], + [ + TransformUtil.Path.empty().append_block("controller").append_port("cs_out_1"), + TransformUtil.Path.empty().append_block("peripheral1").append_port("cs_in"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "cs2_link", + [ + NetPin(["controller"], "1"), + NetPin(["peripheral2"], "4"), + ], + [ + TransformUtil.Path.empty().append_block("controller").append_port("cs_out_2"), + TransformUtil.Path.empty().append_block("peripheral2").append_port("cs_in"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "spi_link.sck", + [ + NetPin(["controller"], "2"), + NetPin(["peripheral1"], "1"), + NetPin(["peripheral2"], "1"), + ], + [ + TransformUtil.Path.empty().append_block("controller").append_port("spi", "sck"), + TransformUtil.Path.empty().append_block("peripheral1").append_port("spi", "sck"), + TransformUtil.Path.empty().append_block("peripheral2").append_port("spi", "sck"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "spi_link.mosi", + [ + NetPin(["controller"], "4"), + NetPin(["peripheral1"], "2"), + NetPin(["peripheral2"], "2"), + ], + [ + TransformUtil.Path.empty().append_block("controller").append_port("spi", "mosi"), + TransformUtil.Path.empty().append_block("peripheral1").append_port("spi", "mosi"), + TransformUtil.Path.empty().append_block("peripheral2").append_port("spi", "mosi"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "spi_link.miso", + [ + NetPin(["controller"], "3"), + NetPin(["peripheral1"], "3"), + NetPin(["peripheral2"], "3"), + ], + [ + TransformUtil.Path.empty().append_block("controller").append_port("spi", "miso"), + TransformUtil.Path.empty().append_block("peripheral1").append_port("spi", "miso"), + TransformUtil.Path.empty().append_block("peripheral2").append_port("spi", "miso"), + ], + ), + net.nets, + ) + + self.assertIn( + NetBlock( + "Resistor_SMD:R_Array_Concave_2x0603", + "R1", + "", + "WeirdSpiController", + ["controller"], + ["controller"], + ["edg.electronics_model.test_bundle_netlist.TestFakeSpiController"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_Array_Concave_2x0603", + "R2", + "", + "WeirdSpiPeripheral", + ["peripheral1"], + ["peripheral1"], + ["edg.electronics_model.test_bundle_netlist.TestFakeSpiPeripheral"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_Array_Concave_2x0603", + "R3", + "", + "WeirdSpiPeripheral", + ["peripheral2"], + ["peripheral2"], + ["edg.electronics_model.test_bundle_netlist.TestFakeSpiPeripheral"], + ), + net.blocks, + ) + + def test_uart_netlist(self) -> None: + net = NetlistTestCase.generate_net(TestUartCircuit) + + self.assertIn( + Net( + "link.a_tx", + [NetPin(["a"], "1"), NetPin(["b"], "2")], + [ + TransformUtil.Path.empty().append_block("a").append_port("port", "tx"), + TransformUtil.Path.empty().append_block("b").append_port("port", "rx"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "link.b_tx", + [NetPin(["a"], "2"), NetPin(["b"], "1")], + [ + TransformUtil.Path.empty().append_block("a").append_port("port", "rx"), + TransformUtil.Path.empty().append_block("b").append_port("port", "tx"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "1k", + ["a"], + ["a"], + ["edg.electronics_model.test_bundle_netlist.TestFakeUartBlock"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R2", + "", + "1k", + ["b"], + ["b"], + ["edg.electronics_model.test_bundle_netlist.TestFakeUartBlock"], + ), + net.blocks, + ) + + def test_can_netlist(self) -> None: + net = NetlistTestCase.generate_net(TestCanCircuit) + + self.assertIn( + Net( + "link.canh", + [NetPin(["node1"], "1"), NetPin(["node2"], "1"), NetPin(["node3"], "1")], + [ + TransformUtil.Path.empty().append_block("node1").append_port("port", "canh"), + TransformUtil.Path.empty().append_block("node2").append_port("port", "canh"), + TransformUtil.Path.empty().append_block("node3").append_port("port", "canh"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "link.canl", + [NetPin(["node1"], "2"), NetPin(["node2"], "2"), NetPin(["node3"], "2")], + [ + TransformUtil.Path.empty().append_block("node1").append_port("port", "canl"), + TransformUtil.Path.empty().append_block("node2").append_port("port", "canl"), + TransformUtil.Path.empty().append_block("node3").append_port("port", "canl"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "120", + ["node1"], + ["node1"], + ["edg.electronics_model.test_bundle_netlist.TestFakeCanBlock"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R2", + "", + "120", + ["node2"], + ["node2"], + ["edg.electronics_model.test_bundle_netlist.TestFakeCanBlock"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R3", + "", + "120", + ["node3"], + ["node3"], + ["edg.electronics_model.test_bundle_netlist.TestFakeCanBlock"], + ), + net.blocks, + ) diff --git a/edg/electronics_model/test_i2c_link.py b/edg/electronics_model/test_i2c_link.py index 8a2363462..0e549e618 100644 --- a/edg/electronics_model/test_i2c_link.py +++ b/edg/electronics_model/test_i2c_link.py @@ -4,61 +4,61 @@ class I2cControllerBlock(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(I2cController()) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(I2cController()) class I2cPullupBlock(Block): - def __init__(self) -> None: - super().__init__() - self.port = self.Port(I2cPullupPort()) + def __init__(self) -> None: + super().__init__() + self.port = self.Port(I2cPullupPort()) class I2cTargetBlock(Block): - def __init__(self, address: IntLike): - super().__init__() - self.port = self.Port(I2cTarget(DigitalBidir(), [address])) + def __init__(self, address: IntLike): + super().__init__() + self.port = self.Port(I2cTarget(DigitalBidir(), [address])) class I2cTest(DesignTop): - def __init__(self) -> None: - super().__init__() - self.controller = self.Block(I2cControllerBlock()) - self.pull = self.Block(I2cPullupBlock()) - self.device1 = self.Block(I2cTargetBlock(1)) - self.device2 = self.Block(I2cTargetBlock(2)) - self.link = self.connect(self.controller.port, self.pull.port, self.device1.port, self.device2.port) + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2cControllerBlock()) + self.pull = self.Block(I2cPullupBlock()) + self.device1 = self.Block(I2cTargetBlock(1)) + self.device2 = self.Block(I2cTargetBlock(2)) + self.link = self.connect(self.controller.port, self.pull.port, self.device1.port, self.device2.port) - self.require(self.controller.port.link().addresses == [1, 2], unchecked=True) + self.require(self.controller.port.link().addresses == [1, 2], unchecked=True) class I2cNoPullTest(DesignTop): - def __init__(self) -> None: - super().__init__() - self.controller = self.Block(I2cControllerBlock()) - self.device1 = self.Block(I2cTargetBlock(1)) - self.link = self.connect(self.controller.port, self.device1.port) + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2cControllerBlock()) + self.device1 = self.Block(I2cTargetBlock(1)) + self.link = self.connect(self.controller.port, self.device1.port) class I2cConflictTest(DesignTop): - def __init__(self) -> None: - super().__init__() - self.controller = self.Block(I2cControllerBlock()) - self.pull = self.Block(I2cPullupBlock()) - self.device1 = self.Block(I2cTargetBlock(1)) - self.device2 = self.Block(I2cTargetBlock(1)) - self.link = self.connect(self.controller.port, self.pull.port, self.device1.port, self.device2.port) + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2cControllerBlock()) + self.pull = self.Block(I2cPullupBlock()) + self.device1 = self.Block(I2cTargetBlock(1)) + self.device2 = self.Block(I2cTargetBlock(1)) + self.link = self.connect(self.controller.port, self.pull.port, self.device1.port, self.device2.port) class I2cTestCase(unittest.TestCase): - def test_i2c(self) -> None: - ScalaCompiler.compile(I2cTest) + def test_i2c(self) -> None: + ScalaCompiler.compile(I2cTest) - def test_i2c_nopull(self) -> None: - with self.assertRaises(CompilerCheckError): - ScalaCompiler.compile(I2cNoPullTest) + def test_i2c_nopull(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(I2cNoPullTest) - def test_i2c_conflict(self) -> None: - with self.assertRaises(CompilerCheckError): - ScalaCompiler.compile(I2cConflictTest) + def test_i2c_conflict(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(I2cConflictTest) diff --git a/edg/electronics_model/test_kicad.py b/edg/electronics_model/test_kicad.py index 047c8d8ee..7ac2b956d 100644 --- a/edg/electronics_model/test_kicad.py +++ b/edg/electronics_model/test_kicad.py @@ -9,29 +9,29 @@ class NetlistTestCase(unittest.TestCase): - def generate_net(self, design: Type[Block]) -> Tuple[str, str]: - compiled = ScalaCompiler.compile(design) - compiled.append_values(RefdesRefinementPass().run(compiled)) + def generate_net(self, design: Type[Block]) -> Tuple[str, str]: + compiled = ScalaCompiler.compile(design) + compiled.append_values(RefdesRefinementPass().run(compiled)) - return NetlistBackend().run(compiled)[0][1], GenerateBom().run(compiled)[0][1] + return NetlistBackend().run(compiled)[0][1], GenerateBom().run(compiled)[0][1] - def test_basic_kicad(self) -> None: - (net, bom) = self.generate_net(test_netlist.TestBasicCircuit) - with open(os.path.splitext(os.path.basename(__file__))[0] + '_basic.net', 'w') as f: - f.write(net) - with open(os.path.splitext(os.path.basename(__file__))[0] + '_basic.csv', 'w') as f: - f.write(bom) + def test_basic_kicad(self) -> None: + (net, bom) = self.generate_net(test_netlist.TestBasicCircuit) + with open(os.path.splitext(os.path.basename(__file__))[0] + "_basic.net", "w") as f: + f.write(net) + with open(os.path.splitext(os.path.basename(__file__))[0] + "_basic.csv", "w") as f: + f.write(bom) - def test_multisink_kicad(self) -> None: - (net, bom) = self.generate_net(test_netlist.TestMultisinkCircuit) - with open(os.path.splitext(os.path.basename(__file__))[0] + '_multisink.net', 'w') as f: - f.write(net) - with open(os.path.splitext(os.path.basename(__file__))[0] + '_multisink.csv', 'w') as f: - f.write(bom) + def test_multisink_kicad(self) -> None: + (net, bom) = self.generate_net(test_netlist.TestMultisinkCircuit) + with open(os.path.splitext(os.path.basename(__file__))[0] + "_multisink.net", "w") as f: + f.write(net) + with open(os.path.splitext(os.path.basename(__file__))[0] + "_multisink.csv", "w") as f: + f.write(bom) - def test_multinet_kicad(self) -> None: - (net, bom) = self.generate_net(test_netlist.TestMultinetCircuit) - with open(os.path.splitext(os.path.basename(__file__))[0] + '_multinet.net', 'w') as f: - f.write(net) - with open(os.path.splitext(os.path.basename(__file__))[0] + '_multinet.csv', 'w') as f: - f.write(bom) + def test_multinet_kicad(self) -> None: + (net, bom) = self.generate_net(test_netlist.TestMultinetCircuit) + with open(os.path.splitext(os.path.basename(__file__))[0] + "_multinet.net", "w") as f: + f.write(net) + with open(os.path.splitext(os.path.basename(__file__))[0] + "_multinet.csv", "w") as f: + f.write(bom) diff --git a/edg/electronics_model/test_kicad_fail.py b/edg/electronics_model/test_kicad_fail.py index c9cabc8e0..ee0b0962c 100644 --- a/edg/electronics_model/test_kicad_fail.py +++ b/edg/electronics_model/test_kicad_fail.py @@ -5,6 +5,7 @@ class KiCadMissingPort(KiCadSchematicBlock): """This block lacks the PORT_A port.""" + def __init__(self) -> None: super().__init__() self.import_kicad(self.file_path("resources", "test_kicad_import.kicad_sch")) @@ -12,6 +13,7 @@ def __init__(self) -> None: class KiCadAliasedPort(KiCadSchematicBlock): """This aliases Test_Net_1 as a port.""" + def __init__(self) -> None: super().__init__() self.Test_Net_1 = self.Port(Passive()) @@ -20,6 +22,7 @@ def __init__(self) -> None: class KiCadAliasedLink(KiCadSchematicBlock): """This aliases Test_Net_1 as an existing link.""" + def __init__(self) -> None: super().__init__() self.Test_Net_1 = 0 diff --git a/edg/electronics_model/test_kicad_import.py b/edg/electronics_model/test_kicad_import.py index f902013c7..c23df53c6 100644 --- a/edg/electronics_model/test_kicad_import.py +++ b/edg/electronics_model/test_kicad_import.py @@ -11,6 +11,7 @@ # Rotation and mirroring are not checked here, but tested in the schematic parser. class KiCadBlock(KiCadSchematicBlock): """Block that has its implementation completely defined in KiCad.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -19,6 +20,7 @@ def __init__(self) -> None: class KiCadBlockAliasedPinName(KiCadSchematicBlock): """Block with a symbol that has the same pin name and number.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -27,6 +29,7 @@ def __init__(self) -> None: class KiCadHierarchyLabelBlock(KiCadSchematicBlock): """Block that uses a hierarchy label for its port mapping.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -35,6 +38,7 @@ def __init__(self) -> None: class KiCadTunnelBlock(KiCadSchematicBlock): """Block that has its implementation completely defined in KiCad, including using net labels as a tunnel.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -43,6 +47,7 @@ def __init__(self) -> None: class KiCadInlineBlock(KiCadSchematicBlock): """Block that has its implementation completely defined in KiCad, using inline Python in the symbol value.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -51,6 +56,7 @@ def __init__(self) -> None: class KiCadInlineBlockBadMultiline(KiCadSchematicBlock): """Schematic with bad multiline definition, starting before Value2""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -60,43 +66,47 @@ def __init__(self) -> None: class KiCadInlineVarsBlock(KiCadSchematicBlock): """Block that has its implementation completely defined in KiCad, using inline Python that references local variables defined in the HDL.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) - self.import_kicad(self.file_path("resources", "test_kicad_import_inline_vars.kicad_sch"), { - 'r1_res': 51*Ohm(tol=0.05), - 'r2_res': 51*Ohm(tol=0.05), - 'c1_cap': 47*uFarad(tol=0.2), - 'in_voltage': (0, 6.3)*Volt, - }) + self.import_kicad( + self.file_path("resources", "test_kicad_import_inline_vars.kicad_sch"), + { + "r1_res": 51 * Ohm(tol=0.05), + "r2_res": 51 * Ohm(tol=0.05), + "c1_cap": 47 * uFarad(tol=0.2), + "in_voltage": (0, 6.3) * Volt, + }, + ) class KiCadCodePartsBock(KiCadSchematicBlock): """Block that has subblocks defined in HDL but connectivity defined in KiCad.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) - self.R1 = self.Block(Resistor(51*Ohm(tol=0.05))) - self.R2 = self.Block(Resistor(51*Ohm(tol=0.05))) - self.C1 = self.Block(Capacitor(47*uFarad(tol=0.2), (0, 6.3)*Volt)) + self.R1 = self.Block(Resistor(51 * Ohm(tol=0.05))) + self.R2 = self.Block(Resistor(51 * Ohm(tol=0.05))) + self.C1 = self.Block(Capacitor(47 * uFarad(tol=0.2), (0, 6.3) * Volt)) self.import_kicad(self.file_path("resources", "test_kicad_import_codeparts.kicad_sch")) class KiCadNodeBlock(KiCadSchematicBlock): """Block that has its implementation completely defined in KiCad.""" + def __init__(self) -> None: super().__init__() - self.R1 = self.Block(Resistor(51*Ohm(tol=0.05))) + self.R1 = self.Block(Resistor(51 * Ohm(tol=0.05))) self.PORT_A = self.Export(self.R1.a) - self.import_kicad(self.file_path("resources", "test_kicad_import_node.kicad_sch"), - nodes={ - 'node': self.R1.b - }) + self.import_kicad(self.file_path("resources", "test_kicad_import_node.kicad_sch"), nodes={"node": self.R1.b}) self.node = self.connect(self.R1.b) # give it a name, must be after the import to not conflict class KiCadPowerBlock(KiCadSchematicBlock): """Block using Power symbols (eg, Vdd, GND) as internal non-named tunnels.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -105,6 +115,7 @@ def __init__(self) -> None: class KiCadModifiedSymbolBlock(KiCadSchematicBlock): """Imports a schematic with a modified (sheet-specific) symbol.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -113,6 +124,7 @@ def __init__(self) -> None: class KiCadBlockAliasedPort(KiCadSchematicBlock): """Block that has a port with the same name as an internal net.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -122,6 +134,7 @@ def __init__(self) -> None: class KiCadBlockOverlappedPort(KiCadSchematicBlock): """Block that has a port with the same name as an internal net, but is also connected to the port.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(Passive()) @@ -177,53 +190,53 @@ def check_connectivity(self, cls: Type[KiCadSchematicBlock]) -> None: constraints = list(map(lambda pair: pair.value, pb.constraints)) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'GND' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'C1' - expected_conn.connected.block_port.ref.steps.add().name = 'neg' + expected_conn.connected.link_port.ref.steps.add().name = "GND" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "C1" + expected_conn.connected.block_port.ref.steps.add().name = "neg" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'GND' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'R2' - expected_conn.connected.block_port.ref.steps.add().name = 'b' + expected_conn.connected.link_port.ref.steps.add().name = "GND" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "R2" + expected_conn.connected.block_port.ref.steps.add().name = "b" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'R1' - expected_conn.connected.block_port.ref.steps.add().name = 'b' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "R1" + expected_conn.connected.block_port.ref.steps.add().name = "b" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'C1' - expected_conn.connected.block_port.ref.steps.add().name = 'pos' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "C1" + expected_conn.connected.block_port.ref.steps.add().name = "pos" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'R2' - expected_conn.connected.block_port.ref.steps.add().name = 'a' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "R2" + expected_conn.connected.block_port.ref.steps.add().name = "a" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'PORT_A' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'R1' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'a' + expected_conn.exported.exterior_port.ref.steps.add().name = "PORT_A" + expected_conn.exported.internal_block_port.ref.steps.add().name = "R1" + expected_conn.exported.internal_block_port.ref.steps.add().name = "a" self.assertIn(expected_conn, constraints) - self.assertIn(edgir.AssignLit(['R1', 'resistance'], Range.from_tolerance(51, 0.05)), constraints) - self.assertIn(edgir.AssignLit(['R2', 'resistance'], Range.from_tolerance(51, 0.05)), constraints) + self.assertIn(edgir.AssignLit(["R1", "resistance"], Range.from_tolerance(51, 0.05)), constraints) + self.assertIn(edgir.AssignLit(["R2", "resistance"], Range.from_tolerance(51, 0.05)), constraints) - self.assertIn(edgir.AssignLit(['C1', 'capacitance'], Range.from_tolerance(47e-6, 0.2)), constraints) - self.assertIn(edgir.AssignLit(['C1', 'voltage'], Range(0, 6.3)), constraints) + self.assertIn(edgir.AssignLit(["C1", "capacitance"], Range.from_tolerance(47e-6, 0.2)), constraints) + self.assertIn(edgir.AssignLit(["C1", "voltage"], Range(0, 6.3)), constraints) diff --git a/edg/electronics_model/test_kicad_import_blackbox.py b/edg/electronics_model/test_kicad_import_blackbox.py index 3f676e22b..eafcbcafd 100644 --- a/edg/electronics_model/test_kicad_import_blackbox.py +++ b/edg/electronics_model/test_kicad_import_blackbox.py @@ -8,6 +8,7 @@ class KiCadBlackboxBlock(KiCadSchematicBlock): """Block with a blackbox part, a sub-blocks that only knows it has a footprint and pins and doesn't map to one of the abstract types.""" + def __init__(self) -> None: super().__init__() self.pwr = self.Port(Passive.empty()) @@ -18,13 +19,13 @@ def __init__(self) -> None: class KiCadBlackboxBlockAutoadapt(KiCadSchematicBlock): """Same example as above, but with typed ports and auto-adaptor generation.""" + def __init__(self) -> None: super().__init__() self.pwr = self.Port(VoltageSink.empty()) self.gnd = self.Port(Ground.empty()) self.out = self.Port(AnalogSource.empty()) - self.import_kicad(self.file_path("resources", "test_kicad_import_blackbox.kicad_sch"), - auto_adapt=True) + self.import_kicad(self.file_path("resources", "test_kicad_import_blackbox.kicad_sch"), auto_adapt=True) class KiCadImportBlackboxTestCase(unittest.TestCase): @@ -34,52 +35,55 @@ def test_import_blackbox(self) -> None: constraints = list(map(lambda pair: pair.value, pb.constraints)) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'pwr' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'U1' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'ports' - expected_conn.exported.internal_block_port.ref.steps.add().allocate = '1' + expected_conn.exported.exterior_port.ref.steps.add().name = "pwr" + expected_conn.exported.internal_block_port.ref.steps.add().name = "U1" + expected_conn.exported.internal_block_port.ref.steps.add().name = "ports" + expected_conn.exported.internal_block_port.ref.steps.add().allocate = "1" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'gnd' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'U1' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'ports' - expected_conn.exported.internal_block_port.ref.steps.add().allocate = '3' + expected_conn.exported.exterior_port.ref.steps.add().name = "gnd" + expected_conn.exported.internal_block_port.ref.steps.add().name = "U1" + expected_conn.exported.internal_block_port.ref.steps.add().name = "ports" + expected_conn.exported.internal_block_port.ref.steps.add().allocate = "3" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'U1' - expected_conn.connected.block_port.ref.steps.add().name = 'ports' - expected_conn.connected.block_port.ref.steps.add().allocate = '2' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "U1" + expected_conn.connected.block_port.ref.steps.add().name = "ports" + expected_conn.connected.block_port.ref.steps.add().allocate = "2" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'res' - expected_conn.connected.block_port.ref.steps.add().name = 'a' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "res" + expected_conn.connected.block_port.ref.steps.add().name = "a" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'out' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'res' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'b' + expected_conn.exported.exterior_port.ref.steps.add().name = "out" + expected_conn.exported.internal_block_port.ref.steps.add().name = "res" + expected_conn.exported.internal_block_port.ref.steps.add().name = "b" self.assertIn(expected_conn, constraints) # resistor not checked, responsibility of another test # U1.kicad_pins not checked, because array assign syntax is wonky - self.assertIn(edgir.AssignLit(['U1', 'kicad_refdes_prefix'], 'U'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_footprint'], 'Package_TO_SOT_SMD:SOT-23'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_part'], 'Sensor_Temperature:MCP9700AT-ETT'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_value'], 'MCP9700AT-ETT'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_datasheet'], 'http://ww1.microchip.com/downloads/en/DeviceDoc/21942e.pdf'), constraints) - - self.assertIn(edgir.AssignLit(['SYM1', 'kicad_refdes_prefix'], 'SYM'), constraints) - self.assertIn(edgir.AssignLit(['SYM1', 'kicad_footprint'], 'Symbol:Symbol_ESD-Logo_CopperTop'), constraints) + self.assertIn(edgir.AssignLit(["U1", "kicad_refdes_prefix"], "U"), constraints) + self.assertIn(edgir.AssignLit(["U1", "kicad_footprint"], "Package_TO_SOT_SMD:SOT-23"), constraints) + self.assertIn(edgir.AssignLit(["U1", "kicad_part"], "Sensor_Temperature:MCP9700AT-ETT"), constraints) + self.assertIn(edgir.AssignLit(["U1", "kicad_value"], "MCP9700AT-ETT"), constraints) + self.assertIn( + edgir.AssignLit(["U1", "kicad_datasheet"], "http://ww1.microchip.com/downloads/en/DeviceDoc/21942e.pdf"), + constraints, + ) + + self.assertIn(edgir.AssignLit(["SYM1", "kicad_refdes_prefix"], "SYM"), constraints) + self.assertIn(edgir.AssignLit(["SYM1", "kicad_footprint"], "Symbol:Symbol_ESD-Logo_CopperTop"), constraints) def test_import_blackbox_autoadapt(self) -> None: # the elaborate_toplevel wrapper is needed since the inner block uses array ports @@ -88,38 +92,38 @@ def test_import_blackbox_autoadapt(self) -> None: # just check an adapter has been generated, don't need to check the details expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'pwr' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(adapter)U1.ports.1' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'dst' + expected_conn.exported.exterior_port.ref.steps.add().name = "pwr" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(adapter)U1.ports.1" + expected_conn.exported.internal_block_port.ref.steps.add().name = "dst" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'gnd' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(adapter)U1.ports.3' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'dst' + expected_conn.exported.exterior_port.ref.steps.add().name = "gnd" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(adapter)U1.ports.3" + expected_conn.exported.internal_block_port.ref.steps.add().name = "dst" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() # this one should be unchanged - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'U1' - expected_conn.connected.block_port.ref.steps.add().name = 'ports' - expected_conn.connected.block_port.ref.steps.add().allocate = '2' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "U1" + expected_conn.connected.block_port.ref.steps.add().name = "ports" + expected_conn.connected.block_port.ref.steps.add().allocate = "2" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'res' - expected_conn.connected.block_port.ref.steps.add().name = 'a' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "res" + expected_conn.connected.block_port.ref.steps.add().name = "a" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'out' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(adapter)res.b' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'dst' + expected_conn.exported.exterior_port.ref.steps.add().name = "out" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(adapter)res.b" + expected_conn.exported.internal_block_port.ref.steps.add().name = "dst" self.assertIn(expected_conn, constraints) # blackbox definition not checked again diff --git a/edg/electronics_model/test_kicad_import_bundle.py b/edg/electronics_model/test_kicad_import_bundle.py index 364ac6b63..cbb2d91a8 100644 --- a/edg/electronics_model/test_kicad_import_bundle.py +++ b/edg/electronics_model/test_kicad_import_bundle.py @@ -3,16 +3,20 @@ from .. import edgir from . import * + class KiCadBundleBlock(KiCadSchematicBlock): """Block where the global labels are a bundle connection.""" + def __init__(self) -> None: super().__init__() self.A = self.Port(UartPort(DigitalBidir.empty())) - self.import_kicad(self.file_path("resources", "test_kicad_import_bundle.kicad_sch"), - conversions={ - 'R1.1': DigitalSource(), # ideal port - 'R1.2': DigitalSink(), # ideal port - }) + self.import_kicad( + self.file_path("resources", "test_kicad_import_bundle.kicad_sch"), + conversions={ + "R1.1": DigitalSource(), # ideal port + "R1.2": DigitalSink(), # ideal port + }, + ) class KiCadImportProtoTestCase(unittest.TestCase): @@ -24,15 +28,15 @@ def test_conversion_block(self) -> None: constraints = list(map(lambda pair: pair.value, pb.constraints)) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'A' - expected_conn.exported.exterior_port.ref.steps.add().name = 'tx' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(adapter)R1.a' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'dst' + expected_conn.exported.exterior_port.ref.steps.add().name = "A" + expected_conn.exported.exterior_port.ref.steps.add().name = "tx" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(adapter)R1.a" + expected_conn.exported.internal_block_port.ref.steps.add().name = "dst" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'A' - expected_conn.exported.exterior_port.ref.steps.add().name = 'rx' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(adapter)R1.b' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'dst' + expected_conn.exported.exterior_port.ref.steps.add().name = "A" + expected_conn.exported.exterior_port.ref.steps.add().name = "rx" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(adapter)R1.b" + expected_conn.exported.internal_block_port.ref.steps.add().name = "dst" self.assertIn(expected_conn, constraints) diff --git a/edg/electronics_model/test_kicad_import_conversion.py b/edg/electronics_model/test_kicad_import_conversion.py index e5bd71627..ee4a0ac9d 100644 --- a/edg/electronics_model/test_kicad_import_conversion.py +++ b/edg/electronics_model/test_kicad_import_conversion.py @@ -6,24 +6,26 @@ class KiCadConversionBlock(KiCadSchematicBlock): """Block generates a Passive-to-VoltageSource adapter.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(VoltageSource.empty()) - self.import_kicad(self.file_path("resources", "test_kicad_import.kicad_sch"), - conversions={ - 'R1.1': VoltageSource() # ideal port - }) + self.import_kicad( + self.file_path("resources", "test_kicad_import.kicad_sch"), + conversions={"R1.1": VoltageSource()}, # ideal port + ) class KiCadBoundaryConversionBlock(KiCadSchematicBlock): """Block generates a Passive-to-VoltageSource adapter on the boundary port.""" + def __init__(self) -> None: super().__init__() self.PORT_A = self.Port(VoltageSource.empty()) - self.import_kicad(self.file_path("resources", "test_kicad_import.kicad_sch"), - conversions={ - 'PORT_A': VoltageSource() # ideal port - }) + self.import_kicad( + self.file_path("resources", "test_kicad_import.kicad_sch"), + conversions={"PORT_A": VoltageSource()}, # ideal port + ) class KiCadImportProtoTestCase(unittest.TestCase): @@ -42,7 +44,7 @@ def validate(self, pb: edgir.HierarchyBlock) -> None: constraints = list(map(lambda pair: pair.value, pb.constraints)) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'PORT_A' - expected_conn.exported.internal_block_port.ref.steps.add().name = '(adapter)R1.a' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'dst' + expected_conn.exported.exterior_port.ref.steps.add().name = "PORT_A" + expected_conn.exported.internal_block_port.ref.steps.add().name = "(adapter)R1.a" + expected_conn.exported.internal_block_port.ref.steps.add().name = "dst" self.assertIn(expected_conn, constraints) diff --git a/edg/electronics_model/test_kicad_schematic_parser.py b/edg/electronics_model/test_kicad_schematic_parser.py index 03b13f2de..10113fcdf 100644 --- a/edg/electronics_model/test_kicad_schematic_parser.py +++ b/edg/electronics_model/test_kicad_schematic_parser.py @@ -7,109 +7,115 @@ def net_to_tuple(net: ParsedNet) -> Tuple[Set[Tuple[Type[Any], str]], Set[str]]: - """Converts a ParsedNet to a tuple of net labels and net pins, so it can be compared during unit testing.""" - labels = set([(x.__class__, x.name) for x in net.labels]) - pins = set([f"{x.refdes}.{x.pin_number}" for x in net.pins]) - return (labels, pins) + """Converts a ParsedNet to a tuple of net labels and net pins, so it can be compared during unit testing.""" + labels = set([(x.__class__, x.name) for x in net.labels]) + pins = set([f"{x.refdes}.{x.pin_number}" for x in net.pins]) + return (labels, pins) class KiCadSchematicParserTest(unittest.TestCase): - def test_kicad(self) -> None: - self.check_schematic_rcs("test_kicad_import.kicad_sch") - - def test_kicad_rot(self) -> None: - self.check_schematic_rcs("test_kicad_import_rot.kicad_sch") - - def test_kicad_tunnel(self) -> None: - self.check_schematic_rcs("test_kicad_import_tunnel.kicad_sch") - - def test_kicad_modified_symbol(self) -> None: - self.check_schematic_rcs("test_kicad_import_modified_symbol.kicad_sch") - - def check_schematic_rcs(self, filename: str) -> None: - with open(os.path.join(os.path.dirname(__file__), "resources", filename), "r") as file: - file_data = file.read() - sch = KiCadSchematic(file_data) - nets = [net_to_tuple(x) for x in sch.nets] - self.assertEqual(len(nets), 3) - self.assertIn(({(KiCadGlobalLabel, 'PORT_A')}, {'R1.1'}), nets) - self.assertIn(({(KiCadLabel, 'node')}, {'R1.2', 'R2.1', 'C1.1'}), nets) - self.assertIn(({(KiCadLabel, 'GND')}, {'R2.2', 'C1.2'}), nets) - - symbols = [(x.refdes, x.lib) for x in sch.symbols] - self.assertIn(('R1', 'Device:R'), symbols) - self.assertIn(('R2', 'Device:R'), symbols) - self.assertIn(('C1', 'Device:C'), symbols) - - def test_kicad_power(self) -> None: - # this one differs because it has the additional power "labels" - with open(os.path.join(os.path.dirname(__file__), "resources", "test_kicad_import_power.kicad_sch"), "r") as file: - file_data = file.read() - sch = KiCadSchematic(file_data) - nets = [net_to_tuple(x) for x in sch.nets] - self.assertEqual(len(nets), 3) - self.assertIn(({(KiCadPowerLabel, '+VDC'), (KiCadGlobalLabel, 'PORT_A')}, {'R1.1'}), nets) - self.assertIn(({(KiCadLabel, 'node')}, {'R1.2', 'R2.1', 'C1.1'}), nets) - self.assertIn(({(KiCadPowerLabel, 'GND')}, {'R2.2', 'C1.2'}), nets) - - symbols = [(x.refdes, x.lib) for x in sch.symbols] - self.assertIn(('R1', 'Device:R'), symbols) - self.assertIn(('R2', 'Device:R'), symbols) - self.assertIn(('C1', 'Device:C'), symbols) - - def test_degenerate_label(self) -> None: - with open(os.path.join(os.path.dirname(__file__), "resources", "test_kicad_import_badlabel.kicad_sch"), "r") as file: - file_data = file.read() - with self.assertRaises(ValueError): - KiCadSchematic(file_data) - - def test_noconnect(self) -> None: - with open(os.path.join(os.path.dirname(__file__), "resources", "test_kicad_import_nc.kicad_sch"), "r") as file: - file_data = file.read() - sch = KiCadSchematic(file_data) - nets = [net_to_tuple(x) for x in sch.nets] - self.assertEqual(len(nets), 3) - self.assertIn((set(), {'R1.1'}), nets) - self.assertIn(({(KiCadLabel, 'node')}, {'R1.2', 'R2.1', 'C1.1'}), nets) - self.assertIn(({(KiCadLabel, 'GND')}, {'R2.2', 'C1.2'}), nets) - - def check_bad_noconnect(self, filename: str) -> None: - with open(os.path.join(os.path.dirname(__file__), "resources", filename), "r") as file: - file_data = file.read() - with self.assertRaises(ValueError): - KiCadSchematic(file_data) - - def test_kicad_noconnect_disconnected(self) -> None: - self.check_bad_noconnect("test_kicad_import_nc_baddis.kicad_sch") - - def test_kicad_noconnect_multiple(self) -> None: - self.check_bad_noconnect("test_kicad_import_nc_badmult.kicad_sch") - - def test_kicad_mirrorx(self) -> None: - self.check_schematic_fet("test_kicad_import_mirrorx.kicad_sch") - - def test_kicad_mirrory(self) -> None: - self.check_schematic_fet("test_kicad_import_mirrory.kicad_sch") - - def test_kicad_mirrory_rot(self) -> None: - self.check_schematic_fet("test_kicad_import_mirrory_rot.kicad_sch") - - def check_schematic_fet(self, filename: str) -> None: - """R and Cs are symmetric and don't test for mirroring well.""" - with open(os.path.join(os.path.dirname(__file__), "resources", filename), "r") as file: - file_data = file.read() - sch = KiCadSchematic(file_data) - nets = [net_to_tuple(x) for x in sch.nets] - - self.assertIn(({(KiCadGlobalLabel, 'drain')}, {'Q1.1'}), nets) - self.assertIn(({(KiCadGlobalLabel, 'gate')}, {'Q1.2'}), nets) - self.assertIn(({(KiCadGlobalLabel, 'source')}, {'Q1.3'}), nets) - - def test_connectedports(self) -> None: - """Schematic with two connected ports without components.""" - with open(os.path.join(os.path.dirname(__file__), "resources", "test_kicad_import_connectedports.kicad_sch"), "r") as file: - file_data = file.read() - sch = KiCadSchematic(file_data) - nets = [net_to_tuple(x) for x in sch.nets] - - self.assertIn(({(KiCadGlobalLabel, 'a'), (KiCadGlobalLabel, 'b')}, set()), nets) + def test_kicad(self) -> None: + self.check_schematic_rcs("test_kicad_import.kicad_sch") + + def test_kicad_rot(self) -> None: + self.check_schematic_rcs("test_kicad_import_rot.kicad_sch") + + def test_kicad_tunnel(self) -> None: + self.check_schematic_rcs("test_kicad_import_tunnel.kicad_sch") + + def test_kicad_modified_symbol(self) -> None: + self.check_schematic_rcs("test_kicad_import_modified_symbol.kicad_sch") + + def check_schematic_rcs(self, filename: str) -> None: + with open(os.path.join(os.path.dirname(__file__), "resources", filename), "r") as file: + file_data = file.read() + sch = KiCadSchematic(file_data) + nets = [net_to_tuple(x) for x in sch.nets] + self.assertEqual(len(nets), 3) + self.assertIn(({(KiCadGlobalLabel, "PORT_A")}, {"R1.1"}), nets) + self.assertIn(({(KiCadLabel, "node")}, {"R1.2", "R2.1", "C1.1"}), nets) + self.assertIn(({(KiCadLabel, "GND")}, {"R2.2", "C1.2"}), nets) + + symbols = [(x.refdes, x.lib) for x in sch.symbols] + self.assertIn(("R1", "Device:R"), symbols) + self.assertIn(("R2", "Device:R"), symbols) + self.assertIn(("C1", "Device:C"), symbols) + + def test_kicad_power(self) -> None: + # this one differs because it has the additional power "labels" + with open( + os.path.join(os.path.dirname(__file__), "resources", "test_kicad_import_power.kicad_sch"), "r" + ) as file: + file_data = file.read() + sch = KiCadSchematic(file_data) + nets = [net_to_tuple(x) for x in sch.nets] + self.assertEqual(len(nets), 3) + self.assertIn(({(KiCadPowerLabel, "+VDC"), (KiCadGlobalLabel, "PORT_A")}, {"R1.1"}), nets) + self.assertIn(({(KiCadLabel, "node")}, {"R1.2", "R2.1", "C1.1"}), nets) + self.assertIn(({(KiCadPowerLabel, "GND")}, {"R2.2", "C1.2"}), nets) + + symbols = [(x.refdes, x.lib) for x in sch.symbols] + self.assertIn(("R1", "Device:R"), symbols) + self.assertIn(("R2", "Device:R"), symbols) + self.assertIn(("C1", "Device:C"), symbols) + + def test_degenerate_label(self) -> None: + with open( + os.path.join(os.path.dirname(__file__), "resources", "test_kicad_import_badlabel.kicad_sch"), "r" + ) as file: + file_data = file.read() + with self.assertRaises(ValueError): + KiCadSchematic(file_data) + + def test_noconnect(self) -> None: + with open(os.path.join(os.path.dirname(__file__), "resources", "test_kicad_import_nc.kicad_sch"), "r") as file: + file_data = file.read() + sch = KiCadSchematic(file_data) + nets = [net_to_tuple(x) for x in sch.nets] + self.assertEqual(len(nets), 3) + self.assertIn((set(), {"R1.1"}), nets) + self.assertIn(({(KiCadLabel, "node")}, {"R1.2", "R2.1", "C1.1"}), nets) + self.assertIn(({(KiCadLabel, "GND")}, {"R2.2", "C1.2"}), nets) + + def check_bad_noconnect(self, filename: str) -> None: + with open(os.path.join(os.path.dirname(__file__), "resources", filename), "r") as file: + file_data = file.read() + with self.assertRaises(ValueError): + KiCadSchematic(file_data) + + def test_kicad_noconnect_disconnected(self) -> None: + self.check_bad_noconnect("test_kicad_import_nc_baddis.kicad_sch") + + def test_kicad_noconnect_multiple(self) -> None: + self.check_bad_noconnect("test_kicad_import_nc_badmult.kicad_sch") + + def test_kicad_mirrorx(self) -> None: + self.check_schematic_fet("test_kicad_import_mirrorx.kicad_sch") + + def test_kicad_mirrory(self) -> None: + self.check_schematic_fet("test_kicad_import_mirrory.kicad_sch") + + def test_kicad_mirrory_rot(self) -> None: + self.check_schematic_fet("test_kicad_import_mirrory_rot.kicad_sch") + + def check_schematic_fet(self, filename: str) -> None: + """R and Cs are symmetric and don't test for mirroring well.""" + with open(os.path.join(os.path.dirname(__file__), "resources", filename), "r") as file: + file_data = file.read() + sch = KiCadSchematic(file_data) + nets = [net_to_tuple(x) for x in sch.nets] + + self.assertIn(({(KiCadGlobalLabel, "drain")}, {"Q1.1"}), nets) + self.assertIn(({(KiCadGlobalLabel, "gate")}, {"Q1.2"}), nets) + self.assertIn(({(KiCadGlobalLabel, "source")}, {"Q1.3"}), nets) + + def test_connectedports(self) -> None: + """Schematic with two connected ports without components.""" + with open( + os.path.join(os.path.dirname(__file__), "resources", "test_kicad_import_connectedports.kicad_sch"), "r" + ) as file: + file_data = file.read() + sch = KiCadSchematic(file_data) + nets = [net_to_tuple(x) for x in sch.nets] + + self.assertIn(({(KiCadGlobalLabel, "a"), (KiCadGlobalLabel, "b")}, set()), nets) diff --git a/edg/electronics_model/test_multipack_netlist.py b/edg/electronics_model/test_multipack_netlist.py index e990499cb..ba652dae1 100644 --- a/edg/electronics_model/test_multipack_netlist.py +++ b/edg/electronics_model/test_multipack_netlist.py @@ -9,113 +9,140 @@ class TestFakeSinkElement(TestBaseFakeSink): - # just exists to not be an abstract part - pass + # just exists to not be an abstract part + pass class TestPackedSink(MultipackBlock): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.elements = self.PackedPart(PackedBlockArray(TestFakeSinkElement())) - self.pos = self.PackedExport(self.elements.ports_array(lambda x: x.pos)) - self.neg = self.PackedExport(self.elements.ports_array(lambda x: x.neg)) + self.elements = self.PackedPart(PackedBlockArray(TestFakeSinkElement())) + self.pos = self.PackedExport(self.elements.ports_array(lambda x: x.pos)) + self.neg = self.PackedExport(self.elements.ports_array(lambda x: x.neg)) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.pos_comb = self.Block(PackedVoltageSource()) - self.connect(self.pos, self.pos_comb.pwr_ins) - self.neg_comb = self.Block(PackedVoltageSource()) - self.connect(self.neg, self.neg_comb.pwr_ins) + self.pos_comb = self.Block(PackedVoltageSource()) + self.connect(self.pos, self.pos_comb.pwr_ins) + self.neg_comb = self.Block(PackedVoltageSource()) + self.connect(self.neg, self.neg_comb.pwr_ins) - self.device = self.Block(TestFakeSink()) - self.connect(self.device.pos, self.pos_comb.pwr_out) - self.connect(self.device.neg, self.neg_comb.pwr_out) + self.device = self.Block(TestFakeSink()) + self.connect(self.device.pos, self.pos_comb.pwr_out) + self.connect(self.device.neg, self.neg_comb.pwr_out) class TestPackedDevices(DesignTop): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestFakeSource()) - self.sink1 = self.Block(TestBaseFakeSink()) - self.sink2 = self.Block(TestBaseFakeSink()) + self.source = self.Block(TestFakeSource()) + self.sink1 = self.Block(TestBaseFakeSink()) + self.sink2 = self.Block(TestBaseFakeSink()) - self.vpos = self.connect(self.source.pos, self.sink1.pos, self.sink2.pos) - self.gnd = self.connect(self.source.neg, self.sink1.neg, self.sink2.neg) + self.vpos = self.connect(self.source.pos, self.sink1.pos, self.sink2.pos) + self.gnd = self.connect(self.source.neg, self.sink1.neg, self.sink2.neg) - @override - def multipack(self) -> None: - self.sink = self.PackedBlock(TestPackedSink()) - self.pack(self.sink.elements.request('1'), ['sink1']) - self.pack(self.sink.elements.request('2'), ['sink2']) + @override + def multipack(self) -> None: + self.sink = self.PackedBlock(TestPackedSink()) + self.pack(self.sink.elements.request("1"), ["sink1"]) + self.pack(self.sink.elements.request("2"), ["sink2"]) class TestInvalidPackedDevices(DesignTop): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source1 = self.Block(TestFakeSource()) - self.sink1 = self.Block(TestBaseFakeSink()) - self.vpos1 = self.connect(self.source1.pos, self.sink1.pos) - self.gnd1 = self.connect(self.source1.neg, self.sink1.neg) + self.source1 = self.Block(TestFakeSource()) + self.sink1 = self.Block(TestBaseFakeSink()) + self.vpos1 = self.connect(self.source1.pos, self.sink1.pos) + self.gnd1 = self.connect(self.source1.neg, self.sink1.neg) - self.source2 = self.Block(TestFakeSource()) - self.sink2 = self.Block(TestBaseFakeSink()) - self.vpos2 = self.connect(self.source2.pos, self.sink2.pos) - self.gnd2 = self.connect(self.source2.neg, self.sink2.neg) + self.source2 = self.Block(TestFakeSource()) + self.sink2 = self.Block(TestBaseFakeSink()) + self.vpos2 = self.connect(self.source2.pos, self.sink2.pos) + self.gnd2 = self.connect(self.source2.neg, self.sink2.neg) - @override - def multipack(self) -> None: - self.sink = self.PackedBlock(TestPackedSink()) - self.pack(self.sink.elements.request('1'), ['sink1']) - self.pack(self.sink.elements.request('2'), ['sink2']) + @override + def multipack(self) -> None: + self.sink = self.PackedBlock(TestPackedSink()) + self.pack(self.sink.elements.request("1"), ["sink1"]) + self.pack(self.sink.elements.request("2"), ["sink2"]) class MultipackNetlistTestCase(unittest.TestCase): - def test_packed_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestPackedDevices) - - self.assertIn(Net('vpos', [ - NetPin(['source'], '1'), - NetPin(['sink', 'device'], '1') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('pos'), - TransformUtil.Path.empty().append_block('sink1').append_port('pos'), - TransformUtil.Path.empty().append_block('sink2').append_port('pos'), - TransformUtil.Path.empty().append_block('sink').append_port('pos', '1'), - TransformUtil.Path.empty().append_block('sink').append_port('pos', '2'), - TransformUtil.Path.empty().append_block('sink', 'pos_comb').append_port('pwr_ins', '1'), - TransformUtil.Path.empty().append_block('sink', 'pos_comb').append_port('pwr_ins', '2'), - TransformUtil.Path.empty().append_block('sink', 'pos_comb').append_port('pwr_out'), - TransformUtil.Path.empty().append_block('sink', 'device').append_port('pos'), - ]), net.nets) - self.assertIn(Net('gnd', [ - NetPin(['source'], '2'), - NetPin(['sink', 'device'], '2') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('neg'), - TransformUtil.Path.empty().append_block('sink1').append_port('neg'), - TransformUtil.Path.empty().append_block('sink2').append_port('neg'), - TransformUtil.Path.empty().append_block('sink').append_port('neg', '1'), - TransformUtil.Path.empty().append_block('sink').append_port('neg', '2'), - TransformUtil.Path.empty().append_block('sink', 'neg_comb').append_port('pwr_ins', '1'), - TransformUtil.Path.empty().append_block('sink', 'neg_comb').append_port('pwr_ins', '2'), - TransformUtil.Path.empty().append_block('sink', 'neg_comb').append_port('pwr_out'), - TransformUtil.Path.empty().append_block('sink', 'device').append_port('neg'), - ]), net.nets) - self.assertIn(NetBlock('Capacitor_SMD:C_0603_1608Metric', 'C1', '', '1uF', - ['source'], ['source'], ['edg.electronics_model.test_netlist.TestFakeSource']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '1k', - ['sink', 'device'], ['sink'], ['edg.electronics_model.test_multipack_netlist.TestPackedSink']), - net.blocks) - - def test_invalid_netlist(self) -> None: - from .NetlistGenerator import InvalidPackingException - with self.assertRaises(InvalidPackingException): - NetlistTestCase.generate_net(TestInvalidPackedDevices) + def test_packed_netlist(self) -> None: + net = NetlistTestCase.generate_net(TestPackedDevices) + + self.assertIn( + Net( + "vpos", + [NetPin(["source"], "1"), NetPin(["sink", "device"], "1")], + [ + TransformUtil.Path.empty().append_block("source").append_port("pos"), + TransformUtil.Path.empty().append_block("sink1").append_port("pos"), + TransformUtil.Path.empty().append_block("sink2").append_port("pos"), + TransformUtil.Path.empty().append_block("sink").append_port("pos", "1"), + TransformUtil.Path.empty().append_block("sink").append_port("pos", "2"), + TransformUtil.Path.empty().append_block("sink", "pos_comb").append_port("pwr_ins", "1"), + TransformUtil.Path.empty().append_block("sink", "pos_comb").append_port("pwr_ins", "2"), + TransformUtil.Path.empty().append_block("sink", "pos_comb").append_port("pwr_out"), + TransformUtil.Path.empty().append_block("sink", "device").append_port("pos"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["sink", "device"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg"), + TransformUtil.Path.empty().append_block("sink1").append_port("neg"), + TransformUtil.Path.empty().append_block("sink2").append_port("neg"), + TransformUtil.Path.empty().append_block("sink").append_port("neg", "1"), + TransformUtil.Path.empty().append_block("sink").append_port("neg", "2"), + TransformUtil.Path.empty().append_block("sink", "neg_comb").append_port("pwr_ins", "1"), + TransformUtil.Path.empty().append_block("sink", "neg_comb").append_port("pwr_ins", "2"), + TransformUtil.Path.empty().append_block("sink", "neg_comb").append_port("pwr_out"), + TransformUtil.Path.empty().append_block("sink", "device").append_port("neg"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Capacitor_SMD:C_0603_1608Metric", + "C1", + "", + "1uF", + ["source"], + ["source"], + ["edg.electronics_model.test_netlist.TestFakeSource"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "1k", + ["sink", "device"], + ["sink"], + ["edg.electronics_model.test_multipack_netlist.TestPackedSink"], + ), + net.blocks, + ) + + def test_invalid_netlist(self) -> None: + from .NetlistGenerator import InvalidPackingException + + with self.assertRaises(InvalidPackingException): + NetlistTestCase.generate_net(TestInvalidPackedDevices) diff --git a/edg/electronics_model/test_netlist.py b/edg/electronics_model/test_netlist.py index ffffd9ad0..39b305843 100644 --- a/edg/electronics_model/test_netlist.py +++ b/edg/electronics_model/test_netlist.py @@ -4,6 +4,7 @@ from typing_extensions import override from .. import edgir + # to avoid re-defining NetBlock, this makes specific imports instead of 'from . import *' from ..core import * from .VoltagePorts import VoltageSource, VoltageSink @@ -14,353 +15,519 @@ # wrapper / convenience constructors def NetPin(block_path: List[str], pin_name: str) -> RawNetPin: - return RawNetPin(TransformUtil.Path(tuple(block_path), (), (), ()), pin_name) - -def NetBlock(footprint: str, refdes: str, part: str, value: str, full_path: List[str], path: List[str], - class_path: List[str]) -> RawNetBlock: - return RawNetBlock(footprint, refdes, part, value, - TransformUtil.Path(tuple(full_path), (), (), ()), path, - [edgir.libpath(cls) for cls in class_path]) + return RawNetPin(TransformUtil.Path(tuple(block_path), (), (), ()), pin_name) + + +def NetBlock( + footprint: str, refdes: str, part: str, value: str, full_path: List[str], path: List[str], class_path: List[str] +) -> RawNetBlock: + return RawNetBlock( + footprint, + refdes, + part, + value, + TransformUtil.Path(tuple(full_path), (), (), ()), + path, + [edgir.libpath(cls) for cls in class_path], + ) class TestFakeSource(FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pos = self.Port(VoltageSource(), optional=True) - self.neg = self.Port(VoltageSource(), optional=True) - - @override - def contents(self) -> None: - super().contents() - self.footprint( # beefy (ok, not really) capacitor - 'C', 'Capacitor_SMD:C_0603_1608Metric', - { - '1': self.pos, - '2': self.neg - }, - value='1uF' - ) + def __init__(self) -> None: + super().__init__() + + self.pos = self.Port(VoltageSource(), optional=True) + self.neg = self.Port(VoltageSource(), optional=True) + + @override + def contents(self) -> None: + super().contents() + self.footprint( # beefy (ok, not really) capacitor + "C", "Capacitor_SMD:C_0603_1608Metric", {"1": self.pos, "2": self.neg}, value="1uF" + ) @abstract_block class TestBaseFakeSink(Block): # abstract base class to support multipacking - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.pos = self.Port(VoltageSink.empty()) - self.neg = self.Port(VoltageSink.empty()) + self.pos = self.Port(VoltageSink.empty()) + self.neg = self.Port(VoltageSink.empty()) class TestFakeSink(TestBaseFakeSink, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.pos.init_from(VoltageSink()) - self.neg.init_from(VoltageSink()) - self.footprint( # load resistor - 'R', 'Resistor_SMD:R_0603_1608Metric', - { - '1': self.pos, - '2': self.neg - }, - value='1k' - ) + @override + def contents(self) -> None: + super().contents() + self.pos.init_from(VoltageSink()) + self.neg.init_from(VoltageSink()) + self.footprint( # load resistor + "R", "Resistor_SMD:R_0603_1608Metric", {"1": self.pos, "2": self.neg}, value="1k" + ) class TestSinglePart(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestFakeSource()) + self.source = self.Block(TestFakeSource()) class TestBasicCircuit(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestFakeSource()) - self.sink = self.Block(TestFakeSink()) + self.source = self.Block(TestFakeSource()) + self.sink = self.Block(TestFakeSink()) - self.vpos = self.connect(self.source.pos, self.sink.pos) - self.gnd = self.connect(self.source.neg, self.sink.neg) + self.vpos = self.connect(self.source.pos, self.sink.pos) + self.gnd = self.connect(self.source.neg, self.sink.neg) class TestMultisinkCircuit(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestFakeSource()) - self.sink1 = self.Block(TestFakeSink()) - self.sink2 = self.Block(TestFakeSink()) # TODO make it 4.7k so it's different value + self.source = self.Block(TestFakeSource()) + self.sink1 = self.Block(TestFakeSink()) + self.sink2 = self.Block(TestFakeSink()) # TODO make it 4.7k so it's different value - self.vpos = self.connect(self.source.pos, self.sink1.pos, self.sink2.pos) - self.gnd = self.connect(self.source.neg, self.sink1.neg, self.sink2.neg) + self.vpos = self.connect(self.source.pos, self.sink1.pos, self.sink2.pos) + self.gnd = self.connect(self.source.neg, self.sink1.neg, self.sink2.neg) class TestFakeAdapter(FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pos_in = self.Port(VoltageSink()) - self.pos_out = self.Port(VoltageSource()) - self.neg = self.Port(VoltageSink()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-223-3_TabPin2', - { - '1': self.neg, - '2': self.pos_out, - '3': self.pos_in, - }, - value='LD1117V33' # not quite correct but roll with it - ) + def __init__(self) -> None: + super().__init__() + + self.pos_in = self.Port(VoltageSink()) + self.pos_out = self.Port(VoltageSource()) + self.neg = self.Port(VoltageSink()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + { + "1": self.neg, + "2": self.pos_out, + "3": self.pos_in, + }, + value="LD1117V33", # not quite correct but roll with it + ) class TestMultinetCircuit(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestFakeSource()) - self.adapter = self.Block(TestFakeAdapter()) - self.sink = self.Block(TestFakeSink()) + self.source = self.Block(TestFakeSource()) + self.adapter = self.Block(TestFakeAdapter()) + self.sink = self.Block(TestFakeSink()) - self.vin = self.connect(self.source.pos, self.adapter.pos_in) - self.vout = self.connect(self.adapter.pos_out, self.sink.pos) - self.gnd = self.connect(self.source.neg, self.adapter.neg, self.sink.neg) + self.vin = self.connect(self.source.pos, self.adapter.pos_in) + self.vout = self.connect(self.adapter.pos_out, self.sink.pos) + self.gnd = self.connect(self.source.neg, self.adapter.neg, self.sink.neg) class TestFakeSinkHierarchy(Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.pos = self.Port(VoltageSink.empty()) - self.neg = self.Port(VoltageSink.empty()) + self.pos = self.Port(VoltageSink.empty()) + self.neg = self.Port(VoltageSink.empty()) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.block = self.Block(TestFakeSink()) + self.block = self.Block(TestFakeSink()) - self.vpos = self.connect(self.pos, self.block.pos) - self.vneg = self.connect(self.neg, self.block.neg) + self.vpos = self.connect(self.pos, self.block.pos) + self.vneg = self.connect(self.neg, self.block.neg) class TestHierarchyCircuit(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestFakeSource()) - self.sink = self.Block(TestFakeSinkHierarchy()) + self.source = self.Block(TestFakeSource()) + self.sink = self.Block(TestFakeSinkHierarchy()) - self.vpos = self.connect(self.source.pos, self.sink.pos) - self.gnd = self.connect(self.source.neg, self.sink.neg) + self.vpos = self.connect(self.source.pos, self.sink.pos) + self.gnd = self.connect(self.source.neg, self.sink.neg) class TestFakeDualSinkHierarchy(Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.pos = self.Port(VoltageSink.empty()) - self.neg = self.Port(VoltageSink.empty()) + self.pos = self.Port(VoltageSink.empty()) + self.neg = self.Port(VoltageSink.empty()) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.block1 = self.Block(TestFakeSink()) - self.block2 = self.Block(TestFakeSink()) + self.block1 = self.Block(TestFakeSink()) + self.block2 = self.Block(TestFakeSink()) - self.vpos = self.connect(self.pos, self.block1.pos, self.block2.pos) - self.vneg = self.connect(self.neg, self.block1.neg, self.block2.neg) + self.vpos = self.connect(self.pos, self.block1.pos, self.block2.pos) + self.vneg = self.connect(self.neg, self.block1.neg, self.block2.neg) class TestDualHierarchyCircuit(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestFakeSource()) - self.sink = self.Block(TestFakeDualSinkHierarchy()) + self.source = self.Block(TestFakeSource()) + self.sink = self.Block(TestFakeDualSinkHierarchy()) - self.vpos = self.connect(self.source.pos, self.sink.pos) - self.gnd = self.connect(self.source.neg, self.sink.neg) + self.vpos = self.connect(self.source.pos, self.sink.pos) + self.gnd = self.connect(self.source.neg, self.sink.neg) class NetlistTestCase(unittest.TestCase): - @staticmethod - def generate_net(design: Type[Block], refinements: Refinements = Refinements()) -> Netlist: - compiled = ScalaCompiler.compile(design, refinements) - compiled.append_values(RefdesRefinementPass().run(compiled)) - return NetlistTransform(compiled).run() - - def test_single_netlist(self) -> None: - net = self.generate_net(TestSinglePart) - - # check that the top-level path element is never pruned, even when the design is one element - self.assertIn(NetBlock('Capacitor_SMD:C_0603_1608Metric', 'C1', '', '1uF', - ['source'], ['source'], - ['edg.electronics_model.test_netlist.TestFakeSource']), net.blocks) - - def test_basic_netlist(self) -> None: - net = self.generate_net(TestBasicCircuit) - - self.assertIn(Net('vpos', [ - NetPin(['source'], '1'), - NetPin(['sink'], '1') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('pos'), - TransformUtil.Path.empty().append_block('sink').append_port('pos'), - ]), net.nets) - self.assertIn(Net('gnd', [ - NetPin(['source'], '2'), - NetPin(['sink'], '2') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('neg'), - TransformUtil.Path.empty().append_block('sink').append_port('neg'), - ]), net.nets) - self.assertIn(NetBlock('Capacitor_SMD:C_0603_1608Metric', 'C1', '', '1uF', - ['source'], ['source'], - ['edg.electronics_model.test_netlist.TestFakeSource']), net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '1k', - ['sink'], ['sink'], - ['edg.electronics_model.test_netlist.TestFakeSink']), net.blocks) - - def test_multisink_netlist(self) -> None: - net = self.generate_net(TestMultisinkCircuit) - - self.assertIn(Net('vpos', [ - NetPin(['source'], '1'), - NetPin(['sink1'], '1'), - NetPin(['sink2'], '1') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('pos'), - TransformUtil.Path.empty().append_block('sink1').append_port('pos'), - TransformUtil.Path.empty().append_block('sink2').append_port('pos'), - ]), net.nets) - self.assertIn(Net('gnd', [ - NetPin(['source'], '2'), - NetPin(['sink1'], '2'), - NetPin(['sink2'], '2') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('neg'), - TransformUtil.Path.empty().append_block('sink1').append_port('neg'), - TransformUtil.Path.empty().append_block('sink2').append_port('neg'), - ]), net.nets) - self.assertIn(NetBlock('Capacitor_SMD:C_0603_1608Metric', 'C1', '', '1uF', - ['source'], ['source'], ['edg.electronics_model.test_netlist.TestFakeSource']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '1k', - ['sink1'], ['sink1'], ['edg.electronics_model.test_netlist.TestFakeSink']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R2', '', '1k', - ['sink2'], ['sink2'], ['edg.electronics_model.test_netlist.TestFakeSink']), - net.blocks) - - def test_multinet_netlist(self) -> None: - net = self.generate_net(TestMultinetCircuit) - - self.assertIn(Net('vin', [ - NetPin(['source'], '1'), - NetPin(['adapter'], '3') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('pos'), - TransformUtil.Path.empty().append_block('adapter').append_port('pos_in'), - ]), net.nets) - self.assertIn(Net('vout', [ - NetPin(['adapter'], '2'), - NetPin(['sink'], '1') - ], [ - TransformUtil.Path.empty().append_block('adapter').append_port('pos_out'), - TransformUtil.Path.empty().append_block('sink').append_port('pos'), - ]), net.nets) - self.assertIn(Net('gnd', [ - NetPin(['source'], '2'), - NetPin(['adapter'], '1'), - NetPin(['sink'], '2') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('neg'), - TransformUtil.Path.empty().append_block('adapter').append_port('neg'), - TransformUtil.Path.empty().append_block('sink').append_port('neg'), - ]), net.nets) - self.assertIn(NetBlock('Capacitor_SMD:C_0603_1608Metric', 'C1', '', '1uF', - ['source'], ['source'], ['edg.electronics_model.test_netlist.TestFakeSource']), - net.blocks) - self.assertIn(NetBlock('Package_TO_SOT_SMD:SOT-223-3_TabPin2', 'U1', '', 'LD1117V33', - ['adapter'], ['adapter'], ['edg.electronics_model.test_netlist.TestFakeAdapter']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '1k', - ['sink'], ['sink'], ['edg.electronics_model.test_netlist.TestFakeSink']), - net.blocks) - - def test_hierarchy_netlist(self) -> None: - net = self.generate_net(TestHierarchyCircuit) - - self.assertIn(Net('vpos', [ - NetPin(['source'], '1'), - NetPin(['sink', 'block'], '1') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('pos'), - TransformUtil.Path.empty().append_block('sink').append_port('pos'), - TransformUtil.Path.empty().append_block('sink', 'block').append_port('pos'), - ]), net.nets) - self.assertIn(Net('gnd', [ - NetPin(['source'], '2'), - NetPin(['sink', 'block'], '2') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('neg'), - TransformUtil.Path.empty().append_block('sink').append_port('neg'), - TransformUtil.Path.empty().append_block('sink', 'block').append_port('neg'), - ]), net.nets) - self.assertIn(NetBlock('Capacitor_SMD:C_0603_1608Metric', 'C1', '', '1uF', - ['source'], ['source'], ['edg.electronics_model.test_netlist.TestFakeSource']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '1k', - ['sink', 'block'], ['sink'], ['edg.electronics_model.test_netlist.TestFakeSinkHierarchy']), - net.blocks) - - def test_dual_hierarchy_netlist(self) -> None: - net = self.generate_net(TestDualHierarchyCircuit) - - self.assertIn(Net('vpos', [ - NetPin(['source'], '1'), - NetPin(['sink', 'block1'], '1'), - NetPin(['sink', 'block2'], '1') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('pos'), - TransformUtil.Path.empty().append_block('sink').append_port('pos'), - TransformUtil.Path.empty().append_block('sink', 'block1').append_port('pos'), - TransformUtil.Path.empty().append_block('sink', 'block2').append_port('pos'), - ]), net.nets) - self.assertIn(Net('gnd', [ - NetPin(['source'], '2'), - NetPin(['sink', 'block1'], '2'), - NetPin(['sink', 'block2'], '2') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('neg'), - TransformUtil.Path.empty().append_block('sink').append_port('neg'), - TransformUtil.Path.empty().append_block('sink', 'block1').append_port('neg'), - TransformUtil.Path.empty().append_block('sink', 'block2').append_port('neg'), - ]), net.nets) - self.assertIn(NetBlock('Capacitor_SMD:C_0603_1608Metric', 'C1', '', '1uF', - ['source'], ['source'], ['edg.electronics_model.test_netlist.TestFakeSource']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '1k', - ['sink', 'block1'], ['sink', 'block1'], - ['edg.electronics_model.test_netlist.TestFakeDualSinkHierarchy', - 'edg.electronics_model.test_netlist.TestFakeSink']), - net.blocks) - self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R2', '', '1k', - ['sink', 'block2'], ['sink', 'block2'], - ['edg.electronics_model.test_netlist.TestFakeDualSinkHierarchy', - 'edg.electronics_model.test_netlist.TestFakeSink']), - net.blocks) + @staticmethod + def generate_net(design: Type[Block], refinements: Refinements = Refinements()) -> Netlist: + compiled = ScalaCompiler.compile(design, refinements) + compiled.append_values(RefdesRefinementPass().run(compiled)) + return NetlistTransform(compiled).run() + + def test_single_netlist(self) -> None: + net = self.generate_net(TestSinglePart) + + # check that the top-level path element is never pruned, even when the design is one element + self.assertIn( + NetBlock( + "Capacitor_SMD:C_0603_1608Metric", + "C1", + "", + "1uF", + ["source"], + ["source"], + ["edg.electronics_model.test_netlist.TestFakeSource"], + ), + net.blocks, + ) + + def test_basic_netlist(self) -> None: + net = self.generate_net(TestBasicCircuit) + + self.assertIn( + Net( + "vpos", + [NetPin(["source"], "1"), NetPin(["sink"], "1")], + [ + TransformUtil.Path.empty().append_block("source").append_port("pos"), + TransformUtil.Path.empty().append_block("sink").append_port("pos"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["sink"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg"), + TransformUtil.Path.empty().append_block("sink").append_port("neg"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Capacitor_SMD:C_0603_1608Metric", + "C1", + "", + "1uF", + ["source"], + ["source"], + ["edg.electronics_model.test_netlist.TestFakeSource"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "1k", + ["sink"], + ["sink"], + ["edg.electronics_model.test_netlist.TestFakeSink"], + ), + net.blocks, + ) + + def test_multisink_netlist(self) -> None: + net = self.generate_net(TestMultisinkCircuit) + + self.assertIn( + Net( + "vpos", + [NetPin(["source"], "1"), NetPin(["sink1"], "1"), NetPin(["sink2"], "1")], + [ + TransformUtil.Path.empty().append_block("source").append_port("pos"), + TransformUtil.Path.empty().append_block("sink1").append_port("pos"), + TransformUtil.Path.empty().append_block("sink2").append_port("pos"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["sink1"], "2"), NetPin(["sink2"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg"), + TransformUtil.Path.empty().append_block("sink1").append_port("neg"), + TransformUtil.Path.empty().append_block("sink2").append_port("neg"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Capacitor_SMD:C_0603_1608Metric", + "C1", + "", + "1uF", + ["source"], + ["source"], + ["edg.electronics_model.test_netlist.TestFakeSource"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "1k", + ["sink1"], + ["sink1"], + ["edg.electronics_model.test_netlist.TestFakeSink"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R2", + "", + "1k", + ["sink2"], + ["sink2"], + ["edg.electronics_model.test_netlist.TestFakeSink"], + ), + net.blocks, + ) + + def test_multinet_netlist(self) -> None: + net = self.generate_net(TestMultinetCircuit) + + self.assertIn( + Net( + "vin", + [NetPin(["source"], "1"), NetPin(["adapter"], "3")], + [ + TransformUtil.Path.empty().append_block("source").append_port("pos"), + TransformUtil.Path.empty().append_block("adapter").append_port("pos_in"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "vout", + [NetPin(["adapter"], "2"), NetPin(["sink"], "1")], + [ + TransformUtil.Path.empty().append_block("adapter").append_port("pos_out"), + TransformUtil.Path.empty().append_block("sink").append_port("pos"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["adapter"], "1"), NetPin(["sink"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg"), + TransformUtil.Path.empty().append_block("adapter").append_port("neg"), + TransformUtil.Path.empty().append_block("sink").append_port("neg"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Capacitor_SMD:C_0603_1608Metric", + "C1", + "", + "1uF", + ["source"], + ["source"], + ["edg.electronics_model.test_netlist.TestFakeSource"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + "U1", + "", + "LD1117V33", + ["adapter"], + ["adapter"], + ["edg.electronics_model.test_netlist.TestFakeAdapter"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "1k", + ["sink"], + ["sink"], + ["edg.electronics_model.test_netlist.TestFakeSink"], + ), + net.blocks, + ) + + def test_hierarchy_netlist(self) -> None: + net = self.generate_net(TestHierarchyCircuit) + + self.assertIn( + Net( + "vpos", + [NetPin(["source"], "1"), NetPin(["sink", "block"], "1")], + [ + TransformUtil.Path.empty().append_block("source").append_port("pos"), + TransformUtil.Path.empty().append_block("sink").append_port("pos"), + TransformUtil.Path.empty().append_block("sink", "block").append_port("pos"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["sink", "block"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg"), + TransformUtil.Path.empty().append_block("sink").append_port("neg"), + TransformUtil.Path.empty().append_block("sink", "block").append_port("neg"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Capacitor_SMD:C_0603_1608Metric", + "C1", + "", + "1uF", + ["source"], + ["source"], + ["edg.electronics_model.test_netlist.TestFakeSource"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "1k", + ["sink", "block"], + ["sink"], + ["edg.electronics_model.test_netlist.TestFakeSinkHierarchy"], + ), + net.blocks, + ) + + def test_dual_hierarchy_netlist(self) -> None: + net = self.generate_net(TestDualHierarchyCircuit) + + self.assertIn( + Net( + "vpos", + [NetPin(["source"], "1"), NetPin(["sink", "block1"], "1"), NetPin(["sink", "block2"], "1")], + [ + TransformUtil.Path.empty().append_block("source").append_port("pos"), + TransformUtil.Path.empty().append_block("sink").append_port("pos"), + TransformUtil.Path.empty().append_block("sink", "block1").append_port("pos"), + TransformUtil.Path.empty().append_block("sink", "block2").append_port("pos"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["sink", "block1"], "2"), NetPin(["sink", "block2"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg"), + TransformUtil.Path.empty().append_block("sink").append_port("neg"), + TransformUtil.Path.empty().append_block("sink", "block1").append_port("neg"), + TransformUtil.Path.empty().append_block("sink", "block2").append_port("neg"), + ], + ), + net.nets, + ) + self.assertIn( + NetBlock( + "Capacitor_SMD:C_0603_1608Metric", + "C1", + "", + "1uF", + ["source"], + ["source"], + ["edg.electronics_model.test_netlist.TestFakeSource"], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R1", + "", + "1k", + ["sink", "block1"], + ["sink", "block1"], + [ + "edg.electronics_model.test_netlist.TestFakeDualSinkHierarchy", + "edg.electronics_model.test_netlist.TestFakeSink", + ], + ), + net.blocks, + ) + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R2", + "", + "1k", + ["sink", "block2"], + ["sink", "block2"], + [ + "edg.electronics_model.test_netlist.TestFakeDualSinkHierarchy", + "edg.electronics_model.test_netlist.TestFakeSink", + ], + ), + net.blocks, + ) diff --git a/edg/electronics_model/test_netlist_wrapper.py b/edg/electronics_model/test_netlist_wrapper.py index 50ab8fbf8..5bfa6df78 100644 --- a/edg/electronics_model/test_netlist_wrapper.py +++ b/edg/electronics_model/test_netlist_wrapper.py @@ -8,65 +8,81 @@ class SinkWrapperBlock(WrapperFootprintBlock): - """Wrapper block with a single footprint for two internal sinks whose footprints are ignored.""" - def __init__(self) -> None: - super().__init__() + """Wrapper block with a single footprint for two internal sinks whose footprints are ignored.""" - self.pos = self.Port(VoltageSink.empty()) - self.neg = self.Port(VoltageSink.empty()) + def __init__(self) -> None: + super().__init__() - @override - def contents(self) -> None: - super().contents() + self.pos = self.Port(VoltageSink.empty()) + self.neg = self.Port(VoltageSink.empty()) - self.model1 = self.Block(TestFakeSink()) - self.model2 = self.Block(TestFakeSink()) - self.vpos = self.connect(self.pos, self.model1.pos, self.model2.pos) - self.gnd = self.connect(self.neg, self.model1.neg, self.model2.neg) + @override + def contents(self) -> None: + super().contents() - self.footprint( # only this footprint shows up - 'L', 'Inductor_SMD:L_0603_1608Metric', # distinct footprint and value from internal blocks - { - '1': self.pos, - '2': self.neg - }, - value='100' - ) + self.model1 = self.Block(TestFakeSink()) + self.model2 = self.Block(TestFakeSink()) + self.vpos = self.connect(self.pos, self.model1.pos, self.model2.pos) + self.gnd = self.connect(self.neg, self.model1.neg, self.model2.neg) + + self.footprint( # only this footprint shows up + "L", + "Inductor_SMD:L_0603_1608Metric", # distinct footprint and value from internal blocks + {"1": self.pos, "2": self.neg}, + value="100", + ) class TestWrapperCircuit(Block): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.source = self.Block(TestFakeSource()) - self.sink = self.Block(SinkWrapperBlock()) + self.source = self.Block(TestFakeSource()) + self.sink = self.Block(SinkWrapperBlock()) - self.vpos = self.connect(self.source.pos, self.sink.pos) - self.gnd = self.connect(self.source.neg, self.sink.neg) + self.vpos = self.connect(self.source.pos, self.sink.pos) + self.gnd = self.connect(self.source.neg, self.sink.neg) class NetlistWrapperTestCase(unittest.TestCase): - def test_warpper_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestWrapperCircuit) - - self.assertIn(NetBlock('Inductor_SMD:L_0603_1608Metric', 'L1', '', '100', - ['sink'], ['sink'], - ['edg.electronics_model.test_netlist_wrapper.SinkWrapperBlock']), net.blocks) - self.assertEqual(len(net.blocks), 2) # should only generate top-level source and sink - - self.assertEqual(len(net.nets), 2) # ensure empty nets pruned - self.assertIn(Net('vpos', [ # ensure extraneous nets not generated - NetPin(['source'], '1'), - NetPin(['sink'], '1') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('pos'), - TransformUtil.Path.empty().append_block('sink').append_port('pos'), - ]), net.nets) - self.assertIn(Net('gnd', [ - NetPin(['source'], '2'), - NetPin(['sink'], '2') - ], [ - TransformUtil.Path.empty().append_block('source').append_port('neg'), - TransformUtil.Path.empty().append_block('sink').append_port('neg'), - ]), net.nets) + def test_warpper_netlist(self) -> None: + net = NetlistTestCase.generate_net(TestWrapperCircuit) + + self.assertIn( + NetBlock( + "Inductor_SMD:L_0603_1608Metric", + "L1", + "", + "100", + ["sink"], + ["sink"], + ["edg.electronics_model.test_netlist_wrapper.SinkWrapperBlock"], + ), + net.blocks, + ) + self.assertEqual(len(net.blocks), 2) # should only generate top-level source and sink + + self.assertEqual(len(net.nets), 2) # ensure empty nets pruned + self.assertIn( + Net( + "vpos", + [NetPin(["source"], "1"), NetPin(["sink"], "1")], # ensure extraneous nets not generated + [ + TransformUtil.Path.empty().append_block("source").append_port("pos"), + TransformUtil.Path.empty().append_block("sink").append_port("pos"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["sink"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg"), + TransformUtil.Path.empty().append_block("sink").append_port("neg"), + ], + ), + net.nets, + ) diff --git a/edg/electronics_model/test_part_parser.py b/edg/electronics_model/test_part_parser.py index c018ebf53..76d27e9fb 100644 --- a/edg/electronics_model/test_part_parser.py +++ b/edg/electronics_model/test_part_parser.py @@ -5,38 +5,38 @@ class PartsTableUtilsTest(unittest.TestCase): - def test_parse_value(self) -> None: - self.assertEqual(PartParserUtil.parse_value('20 nF', 'F'), 20e-9) - self.assertEqual(PartParserUtil.parse_value('20 F', 'F'), 20) - self.assertEqual(PartParserUtil.parse_value('20F', 'F'), 20) - self.assertEqual(PartParserUtil.parse_value('50 kV', 'V'), 50e3) - self.assertEqual(PartParserUtil.parse_value('49.9 GΩ', 'Ω'), 49.9e9) - self.assertEqual(PartParserUtil.parse_value('49G9 Ω', 'Ω'), 49.9e9) + def test_parse_value(self) -> None: + self.assertEqual(PartParserUtil.parse_value("20 nF", "F"), 20e-9) + self.assertEqual(PartParserUtil.parse_value("20 F", "F"), 20) + self.assertEqual(PartParserUtil.parse_value("20F", "F"), 20) + self.assertEqual(PartParserUtil.parse_value("50 kV", "V"), 50e3) + self.assertEqual(PartParserUtil.parse_value("49.9 GΩ", "Ω"), 49.9e9) + self.assertEqual(PartParserUtil.parse_value("49G9 Ω", "Ω"), 49.9e9) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('50 k V', 'V'), None) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('50 kA', 'V'), None) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('50 A', 'V'), None) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('50 k', 'V'), None) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('ducks', 'V'), None) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('50k1 kV', 'V'), None) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('50.1.2 V', 'V'), None) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('lol 20F', 'F'), None) - with self.assertRaises(PartParserUtil.ParseError): - self.assertEqual(PartParserUtil.parse_value('20F no', 'F'), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("50 k V", "V"), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("50 kA", "V"), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("50 A", "V"), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("50 k", "V"), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("ducks", "V"), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("50k1 kV", "V"), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("50.1.2 V", "V"), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("lol 20F", "F"), None) + with self.assertRaises(PartParserUtil.ParseError): + self.assertEqual(PartParserUtil.parse_value("20F no", "F"), None) - def test_parse_tolerance(self) -> None: - self.assertEqual(PartParserUtil.parse_abs_tolerance('±100%', 1, 'X'), Range(1 - 1, 1 + 1)) - self.assertEqual(PartParserUtil.parse_abs_tolerance('±10%', 1, 'X'), Range(1 - 0.1, 1 + 0.1)) - self.assertEqual(PartParserUtil.parse_abs_tolerance('±10%', 5, 'X'), Range(5 - 0.5, 5 + 0.5)) - self.assertEqual(PartParserUtil.parse_abs_tolerance('±10 %', 1, 'X'), Range(1 - 0.1, 1 + 0.1)) - self.assertEqual(PartParserUtil.parse_abs_tolerance('±42.1 ppm', 1, 'X'), Range(1 - 42.1e-6, 1 + 42.1e-6)) - self.assertEqual(PartParserUtil.parse_abs_tolerance('±0.25pF', 1, 'F'), Range(1 - 0.25e-12, 1 + 0.25e-12)) - self.assertEqual(PartParserUtil.parse_abs_tolerance('±0.25pF', 10, 'F'), Range(10 - 0.25e-12, 10 + 0.25e-12)) + def test_parse_tolerance(self) -> None: + self.assertEqual(PartParserUtil.parse_abs_tolerance("±100%", 1, "X"), Range(1 - 1, 1 + 1)) + self.assertEqual(PartParserUtil.parse_abs_tolerance("±10%", 1, "X"), Range(1 - 0.1, 1 + 0.1)) + self.assertEqual(PartParserUtil.parse_abs_tolerance("±10%", 5, "X"), Range(5 - 0.5, 5 + 0.5)) + self.assertEqual(PartParserUtil.parse_abs_tolerance("±10 %", 1, "X"), Range(1 - 0.1, 1 + 0.1)) + self.assertEqual(PartParserUtil.parse_abs_tolerance("±42.1 ppm", 1, "X"), Range(1 - 42.1e-6, 1 + 42.1e-6)) + self.assertEqual(PartParserUtil.parse_abs_tolerance("±0.25pF", 1, "F"), Range(1 - 0.25e-12, 1 + 0.25e-12)) + self.assertEqual(PartParserUtil.parse_abs_tolerance("±0.25pF", 10, "F"), Range(10 - 0.25e-12, 10 + 0.25e-12)) diff --git a/edg/electronics_model/test_partplacer.py b/edg/electronics_model/test_partplacer.py index 7d781a744..efc54f9d4 100644 --- a/edg/electronics_model/test_partplacer.py +++ b/edg/electronics_model/test_partplacer.py @@ -8,16 +8,31 @@ class PartPlacerTestCase(unittest.TestCase): def test_placement(self) -> None: u1 = NetBlock( - footprint="Package_QFP:LQFP-48-1EP_7x7mm_P0.5mm_EP3.6x3.6mm", refdes="U1", part="", value="", - full_path=TransformUtil.Path.empty().append_block("U1"), path=[], class_path=[] + footprint="Package_QFP:LQFP-48-1EP_7x7mm_P0.5mm_EP3.6x3.6mm", + refdes="U1", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("U1"), + path=[], + class_path=[], ) r1 = NetBlock( - footprint="Resistor_SMD:R_0603_1608Metric", refdes="R1", part="", value="", - full_path=TransformUtil.Path.empty().append_block("R1"), path=[], class_path=[] + footprint="Resistor_SMD:R_0603_1608Metric", + refdes="R1", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("R1"), + path=[], + class_path=[], ) r2 = NetBlock( - footprint="Resistor_SMD:R_0603_1608Metric", refdes="R2", part="", value="", - full_path=TransformUtil.Path.empty().append_block("R2"), path=[], class_path=[] + footprint="Resistor_SMD:R_0603_1608Metric", + refdes="R2", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("R2"), + path=[], + class_path=[], ) arranged = arrange_blocks([u1, r1, r2]) self.assertEqual(arranged.elts[0][0], TransformUtil.Path.empty().append_block("U1")) @@ -32,62 +47,113 @@ def test_placement(self) -> None: def test_placement_hierarchical(self) -> None: u1 = NetBlock( - footprint="Package_QFP:LQFP-48-1EP_7x7mm_P0.5mm_EP3.6x3.6mm", refdes="U1", part="", value="", - full_path=TransformUtil.Path.empty().append_block('A').append_block("U1"), path=[], class_path=[] + footprint="Package_QFP:LQFP-48-1EP_7x7mm_P0.5mm_EP3.6x3.6mm", + refdes="U1", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("A").append_block("U1"), + path=[], + class_path=[], ) r1 = NetBlock( - footprint="Resistor_SMD:R_0603_1608Metric", refdes="R1", part="", value="", - full_path=TransformUtil.Path.empty().append_block('A').append_block("R1"), path=[], class_path=[] + footprint="Resistor_SMD:R_0603_1608Metric", + refdes="R1", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("A").append_block("R1"), + path=[], + class_path=[], ) r2 = NetBlock( - footprint="Resistor_SMD:R_0603_1608Metric", refdes="R2", part="", value="", - full_path=TransformUtil.Path.empty().append_block('A').append_block("R2"), path=[], class_path=[] + footprint="Resistor_SMD:R_0603_1608Metric", + refdes="R2", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("A").append_block("R2"), + path=[], + class_path=[], ) r3 = NetBlock( - footprint="Resistor_SMD:R_0603_1608Metric", refdes="R3", part="", value="", - full_path=TransformUtil.Path.empty().append_block('B').append_block("R3"), path=[], class_path=[] + footprint="Resistor_SMD:R_0603_1608Metric", + refdes="R3", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("B").append_block("R3"), + path=[], + class_path=[], ) arranged = arrange_blocks([u1, r1, r2, r3]) self.assertAlmostEqual(arranged.elts[0][1][0], 0, places=2) self.assertAlmostEqual(arranged.elts[0][1][1], 0, places=2) assert isinstance(arranged.elts[0][0], PlacedBlock) - self.assertEqual(arranged.elts[0][0].elts[0][0], TransformUtil.Path.empty().append_block("A").append_block("U1")) + self.assertEqual( + arranged.elts[0][0].elts[0][0], TransformUtil.Path.empty().append_block("A").append_block("U1") + ) self.assertAlmostEqual(arranged.elts[0][0].elts[0][1][0], 5.15, places=2) self.assertAlmostEqual(arranged.elts[0][0].elts[0][1][1], 5.15, places=2) - self.assertEqual(arranged.elts[0][0].elts[1][0], TransformUtil.Path.empty().append_block("A").append_block("R1")) + self.assertEqual( + arranged.elts[0][0].elts[1][0], TransformUtil.Path.empty().append_block("A").append_block("R1") + ) self.assertAlmostEqual(arranged.elts[0][0].elts[1][1][0], 12.78, places=2) self.assertAlmostEqual(arranged.elts[0][0].elts[1][1][1], 0.73, places=2) - self.assertEqual(arranged.elts[0][0].elts[2][0], TransformUtil.Path.empty().append_block("A").append_block("R2")) + self.assertEqual( + arranged.elts[0][0].elts[2][0], TransformUtil.Path.empty().append_block("A").append_block("R2") + ) self.assertAlmostEqual(arranged.elts[0][0].elts[2][1][0], 12.78, places=2) self.assertAlmostEqual(arranged.elts[0][0].elts[2][1][1], 3.19, places=2) self.assertAlmostEqual(arranged.elts[1][1][0], 0, places=2) self.assertAlmostEqual(arranged.elts[1][1][1], 13.3, places=2) assert isinstance(arranged.elts[1][0], PlacedBlock) - self.assertEqual(arranged.elts[1][0].elts[0][0], TransformUtil.Path.empty().append_block("B").append_block("R3")) + self.assertEqual( + arranged.elts[1][0].elts[0][0], TransformUtil.Path.empty().append_block("B").append_block("R3") + ) self.assertAlmostEqual(arranged.elts[1][0].elts[0][1][0], 1.48, places=2) self.assertAlmostEqual(arranged.elts[1][0].elts[0][1][1], 0.73, places=2) flattened = flatten_packed_block(arranged) - self.assertAlmostEqual(flattened[TransformUtil.Path.empty().append_block('A').append_block('U1')][0], 5.15, places=2) - self.assertAlmostEqual(flattened[TransformUtil.Path.empty().append_block('A').append_block('U1')][1], 5.15, places=2) - self.assertAlmostEqual(flattened[TransformUtil.Path.empty().append_block('A').append_block('R1')][0], 12.78, places=2) - self.assertAlmostEqual(flattened[TransformUtil.Path.empty().append_block('A').append_block('R1')][1], 0.73, places=2) - self.assertAlmostEqual(flattened[TransformUtil.Path.empty().append_block('B').append_block('R3')][0], 1.48, places=2) - self.assertAlmostEqual(flattened[TransformUtil.Path.empty().append_block('B').append_block('R3')][1], 14.03, places=2) + self.assertAlmostEqual( + flattened[TransformUtil.Path.empty().append_block("A").append_block("U1")][0], 5.15, places=2 + ) + self.assertAlmostEqual( + flattened[TransformUtil.Path.empty().append_block("A").append_block("U1")][1], 5.15, places=2 + ) + self.assertAlmostEqual( + flattened[TransformUtil.Path.empty().append_block("A").append_block("R1")][0], 12.78, places=2 + ) + self.assertAlmostEqual( + flattened[TransformUtil.Path.empty().append_block("A").append_block("R1")][1], 0.73, places=2 + ) + self.assertAlmostEqual( + flattened[TransformUtil.Path.empty().append_block("B").append_block("R3")][0], 1.48, places=2 + ) + self.assertAlmostEqual( + flattened[TransformUtil.Path.empty().append_block("B").append_block("R3")][1], 14.03, places=2 + ) def test_placement_bbox(self) -> None: r1 = NetBlock( - footprint="Resistor_SMD:R_0603_1608Metric", refdes="R1", part="", value="", - full_path=TransformUtil.Path.empty().append_block("R1"), path=[], class_path=[] + footprint="Resistor_SMD:R_0603_1608Metric", + refdes="R1", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("R1"), + path=[], + class_path=[], ) r2 = NetBlock( - footprint="Resistor_SMD:R_0603_1608Metric", refdes="R2", part="", value="", - full_path=TransformUtil.Path.empty().append_block("R2"), path=[], class_path=[] + footprint="Resistor_SMD:R_0603_1608Metric", + refdes="R2", + part="", + value="", + full_path=TransformUtil.Path.empty().append_block("R2"), + path=[], + class_path=[], + ) + arranged = arrange_blocks( + [r1, r2], [BlackBoxBlock(TransformUtil.Path.empty().append_block("box"), (-5, -5, 5, 5))] ) - arranged = arrange_blocks([r1, r2], - [BlackBoxBlock(TransformUtil.Path.empty().append_block("box"), (-5, -5, 5, 5))]) self.assertEqual(arranged.elts[0][0], TransformUtil.Path.empty().append_block("box")) self.assertAlmostEqual(arranged.elts[0][1][0], 5, places=2) self.assertAlmostEqual(arranged.elts[0][1][1], 5, places=2) @@ -96,4 +162,4 @@ def test_placement_bbox(self) -> None: self.assertAlmostEqual(arranged.elts[1][1][1], 0.73, places=2) self.assertEqual(arranged.elts[2][0], TransformUtil.Path.empty().append_block("R2")) self.assertAlmostEqual(arranged.elts[2][1][0], 12.48, places=2) - self.assertAlmostEqual(arranged.elts[2][1][1], 3.19, places=2) \ No newline at end of file + self.assertAlmostEqual(arranged.elts[2][1][1], 3.19, places=2) diff --git a/edg/electronics_model/test_units.py b/edg/electronics_model/test_units.py index 1eeb3ce84..e9ecf2e25 100644 --- a/edg/electronics_model/test_units.py +++ b/edg/electronics_model/test_units.py @@ -4,9 +4,9 @@ class UnitsTestCase(unittest.TestCase): - def test_units(self) -> None: - self.assertEqual(UnitUtils.num_to_prefix(1, 3), '1') - self.assertEqual(UnitUtils.num_to_prefix(1000, 3), '1k') - self.assertEqual(UnitUtils.num_to_prefix(0.001, 3), '1m') - self.assertEqual(UnitUtils.num_to_prefix(4700, 3), '4.7k') - self.assertEqual(UnitUtils.num_to_prefix(0.1e-6, 3), '100n') + def test_units(self) -> None: + self.assertEqual(UnitUtils.num_to_prefix(1, 3), "1") + self.assertEqual(UnitUtils.num_to_prefix(1000, 3), "1k") + self.assertEqual(UnitUtils.num_to_prefix(0.001, 3), "1m") + self.assertEqual(UnitUtils.num_to_prefix(4700, 3), "4.7k") + self.assertEqual(UnitUtils.num_to_prefix(0.1e-6, 3), "100n") diff --git a/edg/electronics_model/test_voltage_bridge.py b/edg/electronics_model/test_voltage_bridge.py index 828804407..7bb8168fb 100644 --- a/edg/electronics_model/test_voltage_bridge.py +++ b/edg/electronics_model/test_voltage_bridge.py @@ -7,9 +7,9 @@ class VoltageBridgeTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = VoltageSinkBridge()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = VoltageSinkBridge()._elaborated_def_to_proto() - def test_metadata(self) -> None: - self.assertIn('nets', self.pb.meta.members.node) \ No newline at end of file + def test_metadata(self) -> None: + self.assertIn("nets", self.pb.meta.members.node) diff --git a/edg/electronics_model/test_voltage_link.py b/edg/electronics_model/test_voltage_link.py index 7cba76232..1e8ca8c60 100644 --- a/edg/electronics_model/test_voltage_link.py +++ b/edg/electronics_model/test_voltage_link.py @@ -6,9 +6,9 @@ class VoltageLinkTestCase(unittest.TestCase): - @override - def setUp(self) -> None: - self.pb = VoltageLink()._elaborated_def_to_proto() + @override + def setUp(self) -> None: + self.pb = VoltageLink()._elaborated_def_to_proto() - def test_metadata(self) -> None: - self.assertIn('nets', self.pb.meta.members.node) + def test_metadata(self) -> None: + self.assertIn("nets", self.pb.meta.members.node) diff --git a/edg/hdl_server/__main__.py b/edg/hdl_server/__main__.py index 9e737f5fe..fe0b5f82e 100644 --- a/edg/hdl_server/__main__.py +++ b/edg/hdl_server/__main__.py @@ -14,157 +14,176 @@ class LibraryElementIndexer: - """Indexer for libraries, recursively searches modules and their LibraryElements.""" - def __init__(self) -> None: - self.seen_modules: Set[ModuleType] = set() - self.seen_elements: Set[Type[LibraryElement]] = set() - - def index_module(self, module: ModuleType) -> Set[Type[LibraryElement]]: - assert not self.seen_elements and not self.seen_modules - self._search_module(module) - return self.seen_elements - - def _search_module(self, module: ModuleType) -> None: - # avoid repeated work and re-indexing modules - if (module.__name__ in sys.builtin_module_names - or not hasattr(module, '__file__') # apparently load six.moves breaks - or module in self.seen_modules): - return - self.seen_modules.add(module) - - for (name, member) in inspect.getmembers(module): - if inspect.ismodule(member): # recurse into visible modules - self._search_module(member) - - if inspect.isclass(member) and issubclass(member, LibraryElement) and not issubclass(member, DesignTop) \ - and member not in self.seen_elements \ - and (member, NonLibraryProperty) not in member._elt_properties: # process elements - self.seen_elements.add(member) - - for mro in member.mro(): - self._search_module(importlib.import_module(mro.__module__)) - - if issubclass(member, Port): # TODO for some reason, Links not in __init__ are sometimes not found - obj = member() # TODO can these be class definitions? - if hasattr(obj, 'link_type'): - self._search_module(importlib.import_module(obj.link_type.__module__)) - - -LibraryElementType = TypeVar('LibraryElementType', bound=LibraryElement) + """Indexer for libraries, recursively searches modules and their LibraryElements.""" + + def __init__(self) -> None: + self.seen_modules: Set[ModuleType] = set() + self.seen_elements: Set[Type[LibraryElement]] = set() + + def index_module(self, module: ModuleType) -> Set[Type[LibraryElement]]: + assert not self.seen_elements and not self.seen_modules + self._search_module(module) + return self.seen_elements + + def _search_module(self, module: ModuleType) -> None: + # avoid repeated work and re-indexing modules + if ( + module.__name__ in sys.builtin_module_names + or not hasattr(module, "__file__") # apparently load six.moves breaks + or module in self.seen_modules + ): + return + self.seen_modules.add(module) + + for name, member in inspect.getmembers(module): + if inspect.ismodule(member): # recurse into visible modules + self._search_module(member) + + if ( + inspect.isclass(member) + and issubclass(member, LibraryElement) + and not issubclass(member, DesignTop) + and member not in self.seen_elements + and (member, NonLibraryProperty) not in member._elt_properties + ): # process elements + self.seen_elements.add(member) + + for mro in member.mro(): + self._search_module(importlib.import_module(mro.__module__)) + + if issubclass(member, Port): # TODO for some reason, Links not in __init__ are sometimes not found + obj = member() # TODO can these be class definitions? + if hasattr(obj, "link_type"): + self._search_module(importlib.import_module(obj.link_type.__module__)) + + +LibraryElementType = TypeVar("LibraryElementType", bound=LibraryElement) + + def elaborate_class(elt_cls: Type[LibraryElementType]) -> Tuple[LibraryElementType, edgir.Library.NS.Val]: - obj = elt_cls() - assert (elt_cls, NonLibraryProperty) not in elt_cls._elt_properties.keys(), \ - f"tried to elaborate non-library {elt_cls.__name__}" - - if isinstance(obj, Block): - block_proto = builder.elaborate_toplevel(obj) - return obj, edgir.Library.NS.Val(hierarchy_block=block_proto) - elif isinstance(obj, Link): - link_proto = builder.elaborate_toplevel(obj) - assert isinstance(link_proto, edgir.Link) # TODO this needs to be cleaned up - return obj, edgir.Library.NS.Val(link=link_proto) - elif isinstance(obj, Bundle): # TODO: note Bundle extends Port, so this must come first - return obj, edgir.Library.NS.Val(bundle=obj._def_to_proto()) - elif isinstance(obj, Port): - return obj, edgir.Library.NS.Val(port=cast(edgir.Port, obj._def_to_proto())) - else: - raise RuntimeError(f"didn't match type of library element {elt_cls}") - - -LibraryClassType = TypeVar('LibraryClassType') -def class_from_library(elt: edgir.LibraryPath, expected_superclass: Type[LibraryClassType]) -> \ - Type[LibraryClassType]: - elt_split = elt.target.name.split('.') - elt_module = importlib.import_module('.'.join(elt_split[:-1])) - assert inspect.ismodule(elt_module) - cls = getattr(elt_module, elt_split[-1]) - assert issubclass(cls, expected_superclass) - return cls # type: ignore + obj = elt_cls() + assert ( + elt_cls, + NonLibraryProperty, + ) not in elt_cls._elt_properties.keys(), f"tried to elaborate non-library {elt_cls.__name__}" + + if isinstance(obj, Block): + block_proto = builder.elaborate_toplevel(obj) + return obj, edgir.Library.NS.Val(hierarchy_block=block_proto) + elif isinstance(obj, Link): + link_proto = builder.elaborate_toplevel(obj) + assert isinstance(link_proto, edgir.Link) # TODO this needs to be cleaned up + return obj, edgir.Library.NS.Val(link=link_proto) + elif isinstance(obj, Bundle): # TODO: note Bundle extends Port, so this must come first + return obj, edgir.Library.NS.Val(bundle=obj._def_to_proto()) + elif isinstance(obj, Port): + return obj, edgir.Library.NS.Val(port=cast(edgir.Port, obj._def_to_proto())) + else: + raise RuntimeError(f"didn't match type of library element {elt_cls}") + + +LibraryClassType = TypeVar("LibraryClassType") + + +def class_from_library(elt: edgir.LibraryPath, expected_superclass: Type[LibraryClassType]) -> Type[LibraryClassType]: + elt_split = elt.target.name.split(".") + elt_module = importlib.import_module(".".join(elt_split[:-1])) + assert inspect.ismodule(elt_module) + cls = getattr(elt_module, elt_split[-1]) + assert issubclass(cls, expected_superclass) + return cls # type: ignore def process_request(request: edgrpc.HdlRequest) -> Optional[edgrpc.HdlResponse]: - response = edgrpc.HdlResponse() - try: - if request.HasField('index_module'): - module = importlib.import_module(request.index_module.name) - library = LibraryElementIndexer() - indexed = [edgir.LibraryPath(target=edgir.LocalStep(name=indexed._static_def_name())) - for indexed in library.index_module(module)] - response.index_module.indexed.extend(indexed) - elif request.HasField('get_library_element'): - cls = class_from_library(request.get_library_element.element, - LibraryElement) # type: ignore - obj, obj_proto = elaborate_class(cls) - - response.get_library_element.element.CopyFrom(obj_proto) - if isinstance(obj, DesignTop): - obj.refinements().populate_proto(response.get_library_element.refinements) - elif request.HasField('elaborate_generator'): - generator_type = class_from_library(request.elaborate_generator.element, - GeneratorBlock) - generator_obj = generator_type() - - response.elaborate_generator.generated.CopyFrom(builder.elaborate_toplevel( - generator_obj, - is_generator=True, - generate_values=[(value.path, value.value) for value in request.elaborate_generator.values])) - elif request.HasField('run_refinement'): - refinement_pass_class = class_from_library(request.run_refinement.refinement_pass, - BaseRefinementPass) # type: ignore - refinement_pass = refinement_pass_class() - - refinement_results = refinement_pass.run( - CompiledDesign.from_request(request.run_refinement.design, request.run_refinement.solvedValues)) - response.run_refinement.SetInParent() - for path, refinement_result in refinement_results: - new_value = response.run_refinement.newValues.add() - new_value.path.CopyFrom(path) - new_value.value.CopyFrom(refinement_result) - elif request.HasField('run_backend'): - backend_class = class_from_library(request.run_backend.backend, - BaseBackend) # type: ignore - backend = backend_class() - - backend_results = backend.run( - CompiledDesign.from_request(request.run_backend.design, request.run_backend.solvedValues), - dict(request.run_backend.arguments)) - response.run_backend.SetInParent() - for path, backend_result in backend_results: - response_result = response.run_backend.results.add() - response_result.path.CopyFrom(path) - response_result.text = backend_result - elif request.HasField('get_proto_version'): - response.get_proto_version = EDG_PROTO_VERSION - else: - return None - except BaseException as e: - import traceback - # exception formatting from https://stackoverflow.com/questions/4564559/get-exception-description-and-stack-trace-which-caused-an-exception-all-as-a-st response = edgrpc.HdlResponse() - response.error.error = repr(e) - response.error.traceback = "".join(traceback.TracebackException.from_exception(e).format()) - # also print it, to preserve the usual behavior of errors in Python - traceback.print_exc() - return response + try: + if request.HasField("index_module"): + module = importlib.import_module(request.index_module.name) + library = LibraryElementIndexer() + indexed = [ + edgir.LibraryPath(target=edgir.LocalStep(name=indexed._static_def_name())) + for indexed in library.index_module(module) + ] + response.index_module.indexed.extend(indexed) + elif request.HasField("get_library_element"): + cls = class_from_library(request.get_library_element.element, LibraryElement) # type: ignore + obj, obj_proto = elaborate_class(cls) + + response.get_library_element.element.CopyFrom(obj_proto) + if isinstance(obj, DesignTop): + obj.refinements().populate_proto(response.get_library_element.refinements) + elif request.HasField("elaborate_generator"): + generator_type = class_from_library(request.elaborate_generator.element, GeneratorBlock) + generator_obj = generator_type() + + response.elaborate_generator.generated.CopyFrom( + builder.elaborate_toplevel( + generator_obj, + is_generator=True, + generate_values=[(value.path, value.value) for value in request.elaborate_generator.values], + ) + ) + elif request.HasField("run_refinement"): + refinement_pass_class = class_from_library( + request.run_refinement.refinement_pass, BaseRefinementPass # type: ignore + ) + refinement_pass = refinement_pass_class() + + refinement_results = refinement_pass.run( + CompiledDesign.from_request(request.run_refinement.design, request.run_refinement.solvedValues) + ) + response.run_refinement.SetInParent() + for path, refinement_result in refinement_results: + new_value = response.run_refinement.newValues.add() + new_value.path.CopyFrom(path) + new_value.value.CopyFrom(refinement_result) + elif request.HasField("run_backend"): + backend_class = class_from_library(request.run_backend.backend, BaseBackend) # type: ignore + backend = backend_class() + + backend_results = backend.run( + CompiledDesign.from_request(request.run_backend.design, request.run_backend.solvedValues), + dict(request.run_backend.arguments), + ) + response.run_backend.SetInParent() + for path, backend_result in backend_results: + response_result = response.run_backend.results.add() + response_result.path.CopyFrom(path) + response_result.text = backend_result + elif request.HasField("get_proto_version"): + response.get_proto_version = EDG_PROTO_VERSION + else: + return None + except BaseException as e: + import traceback + + # exception formatting from https://stackoverflow.com/questions/4564559/get-exception-description-and-stack-trace-which-caused-an-exception-all-as-a-st + response = edgrpc.HdlResponse() + response.error.error = repr(e) + response.error.traceback = "".join(traceback.TracebackException.from_exception(e).format()) + # also print it, to preserve the usual behavior of errors in Python + traceback.print_exc() + return response + def run_server() -> None: - stdin_deserializer = BufferDeserializer(edgrpc.HdlRequest, sys.stdin.buffer) - stdout_serializer = BufferSerializer[edgrpc.HdlResponse](sys.stdout.buffer) + stdin_deserializer = BufferDeserializer(edgrpc.HdlRequest, sys.stdin.buffer) + stdout_serializer = BufferSerializer[edgrpc.HdlResponse](sys.stdout.buffer) - while True: - request = stdin_deserializer.read() - if request is None: # end of stream - sys.exit(0) + while True: + request = stdin_deserializer.read() + if request is None: # end of stream + sys.exit(0) - response = process_request(request) - if response is None: - raise RuntimeError(f"Unknown request {request}") + response = process_request(request) + if response is None: + raise RuntimeError(f"Unknown request {request}") - sys.stdout.buffer.write(stdin_deserializer.read_stdout()) # forward prints and stuff - stdout_serializer.write(response) + sys.stdout.buffer.write(stdin_deserializer.read_stdout()) # forward prints and stuff + stdout_serializer.write(response) # In some cases stdout seems to buffer excessively, in which case starting python with -u seems to work # https://stackoverflow.com/a/35467658/5875811 -if __name__ == '__main__': - run_server() +if __name__ == "__main__": + run_server() diff --git a/edg/jlcparts/JlcPartsBase.py b/edg/jlcparts/JlcPartsBase.py index e0c62d464..d4b8a35fa 100644 --- a/edg/jlcparts/JlcPartsBase.py +++ b/edg/jlcparts/JlcPartsBase.py @@ -17,7 +17,7 @@ class JlcPartsFile(BaseModel): category: str components: list[list[Any]] # index-matched with schema - jlcpart_schema: list[str] = Field(..., alias='schema') + jlcpart_schema: list[str] = Field(..., alias="schema") class JlcPartsAttributeEntry(BaseModel): @@ -26,12 +26,15 @@ class JlcPartsAttributeEntry(BaseModel): values: dict[str, tuple[Any, str]] -ParsedType = TypeVar('ParsedType') # can't be inside the class or it gets confused as a pydantic model entry +ParsedType = TypeVar("ParsedType") # can't be inside the class or it gets confused as a pydantic model entry + class JlcPartsAttributes(RootModel[Dict[str, JlcPartsAttributeEntry]]): root: dict[str, JlcPartsAttributeEntry] - def get(self, key: str, expected_type: Type[ParsedType], default: Optional[ParsedType] = None, sub: str = 'default') -> ParsedType: + def get( + self, key: str, expected_type: Type[ParsedType], default: Optional[ParsedType] = None, sub: str = "default" + ) -> ParsedType: """Utility function that gets an attribute of the specified name, checking that it is the expected type or returning some default (if specified).""" if key not in self.root: @@ -77,6 +80,7 @@ class JlcPartsStockFile(RootModel[Dict[str, int]]): class JlcPartsBase(JlcPart, PartsTableAreaSelector, PartsTableFootprintFilter): """Base class parsing parts from https://github.com/yaqwsx/jlcparts""" + _config_parts_root_dir: Optional[str] = None _config_min_stock: int = 250 @@ -96,8 +100,9 @@ def config_root_dir(root_dir: str) -> None: """Configures the root dir that contains the data files from jlcparts, eg CapacitorsMultilayer_Ceramic_Capacitors_MLCC___SMDakaSMT.json.gz This setting is on a JlcPartsBase-wide basis.""" - assert JlcPartsBase._config_parts_root_dir is None, \ - f"attempted to reassign config_root_dir, was {JlcPartsBase._config_parts_root_dir}, new {root_dir}" + assert ( + JlcPartsBase._config_parts_root_dir is None + ), f"attempted to reassign config_root_dir, was {JlcPartsBase._config_parts_root_dir}, new {root_dir}" JlcPartsBase._config_parts_root_dir = root_dir _JLC_PARTS_FILE_NAMES: ClassVar[List[str]] # set by subclass @@ -112,8 +117,9 @@ def _make_table(cls) -> PartsTable: return cls._cached_table @classmethod - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes)\ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: """Given an entry from jlcparts and row pre-populated with metadata, adds category-specific data to the row (in-place), and returns the row (or None, if it failed to parse and the row should be discarded).""" raise NotImplementedError @@ -124,16 +130,18 @@ def _parse_table(cls) -> PartsTable: jlcparts_dir = os.environ.get("JLCPARTS_DIR") if jlcparts_dir is None: jlcparts_dir = cls._config_parts_root_dir - assert jlcparts_dir is not None, "no jlcparts data directory specified, either "\ - "set JLCPARTS_DIR environment variable or call JlcPartsBase.config_root_dir "\ - "with jlcparts data folder" + assert jlcparts_dir is not None, ( + "no jlcparts data directory specified, either " + "set JLCPARTS_DIR environment variable or call JlcPartsBase.config_root_dir " + "with jlcparts data folder" + ) rows: List[PartsTableRow] = [] for filename in cls._JLC_PARTS_FILE_NAMES: - with gzip.open(os.path.join(jlcparts_dir, filename + kTableFilenamePostfix), 'r') as f: + with gzip.open(os.path.join(jlcparts_dir, filename + kTableFilenamePostfix), "r") as f: data = JlcPartsFile.model_validate_json(f.read()) - with open(os.path.join(jlcparts_dir, filename + kStockFilenamePostfix), 'r') as f: + with open(os.path.join(jlcparts_dir, filename + kStockFilenamePostfix), "r") as f: stocking = JlcPartsStockFile.model_validate_json(f.read()) lcsc_index = data.jlcpart_schema.index("lcsc") diff --git a/edg/jlcparts/JlcPartsBjt.py b/edg/jlcparts/JlcPartsBjt.py index 8120de599..5e44d71f9 100644 --- a/edg/jlcparts/JlcPartsBjt.py +++ b/edg/jlcparts/JlcPartsBjt.py @@ -10,26 +10,31 @@ class JlcPartsBjt(PartsTableSelectorFootprint, JlcPartsBase, TableBjt): _JLC_PARTS_FILE_NAMES = ["TransistorsBipolar_Transistors___BJT"] _CHANNEL_MAP = { - 'NPN': 'NPN', - 'PNP': 'PNP', + "NPN": "NPN", + "PNP": "PNP", } @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: row_dict[cls.KICAD_FOOTPRINT] = JlcBjt.PACKAGE_FOOTPRINT_MAP[package] row_dict[cls.CHANNEL] = cls._CHANNEL_MAP[attributes.get("Transistor type", str)] - row_dict[cls.VCE_RATING] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Collector-emitter breakdown voltage (vceo)", str), 'V')) - row_dict[cls.ICE_RATING] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Collector current (ic)", str), 'A')) - row_dict[cls.GAIN] = Range.exact(PartParserUtil.parse_value( - attributes.get("Dc current gain (hfe@ic,vce)", str).split('@')[0], '')) + row_dict[cls.VCE_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Collector-emitter breakdown voltage (vceo)", str), "V") + ) + row_dict[cls.ICE_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Collector current (ic)", str), "A") + ) + row_dict[cls.GAIN] = Range.exact( + PartParserUtil.parse_value(attributes.get("Dc current gain (hfe@ic,vce)", str).split("@")[0], "") + ) row_dict[cls.POWER_RATING] = Range.zero_to_upper( - attributes.get("Power dissipation (pd)", float, sub='power')) + attributes.get("Power dissipation (pd)", float, sub="power") + ) return row_dict except (KeyError, TypeError, PartParserUtil.ParseError): diff --git a/edg/jlcparts/JlcPartsBoardTop.py b/edg/jlcparts/JlcPartsBoardTop.py index 791f2a94c..0588f05ad 100644 --- a/edg/jlcparts/JlcPartsBoardTop.py +++ b/edg/jlcparts/JlcPartsBoardTop.py @@ -14,25 +14,26 @@ class JlcPartsRefinements(DesignTop): - """List of refinements that use JlcParts - mix this into a BoardTop""" - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - class_refinements=[ - (Resistor, JlcPartsResistorSmd), - (Capacitor, JlcPartsMlcc), - (Inductor, JlcPartsInductor), - (Diode, JlcPartsDiode), - (ZenerDiode, JlcPartsZenerDiode), - (Led, JlcPartsLed), - (Bjt, JlcPartsBjt), - (Fet, JlcPartsFet), - (SwitchFet, JlcPartsSwitchFet), - (PptcFuse, JlcPartsPptcFuse), - (FerriteBead, JlcPartsFerriteBead) - ], - class_values=[ # realistically only RCs are going to likely be basic parts - (JlcPartsResistorSmd, ['require_basic_part'], True), - (JlcPartsMlcc, ['require_basic_part'], True), - ], - ) + """List of refinements that use JlcParts - mix this into a BoardTop""" + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + class_refinements=[ + (Resistor, JlcPartsResistorSmd), + (Capacitor, JlcPartsMlcc), + (Inductor, JlcPartsInductor), + (Diode, JlcPartsDiode), + (ZenerDiode, JlcPartsZenerDiode), + (Led, JlcPartsLed), + (Bjt, JlcPartsBjt), + (Fet, JlcPartsFet), + (SwitchFet, JlcPartsSwitchFet), + (PptcFuse, JlcPartsPptcFuse), + (FerriteBead, JlcPartsFerriteBead), + ], + class_values=[ # realistically only RCs are going to likely be basic parts + (JlcPartsResistorSmd, ["require_basic_part"], True), + (JlcPartsMlcc, ["require_basic_part"], True), + ], + ) diff --git a/edg/jlcparts/JlcPartsDiode.py b/edg/jlcparts/JlcPartsDiode.py index d9713108f..9880b3991 100644 --- a/edg/jlcparts/JlcPartsDiode.py +++ b/edg/jlcparts/JlcPartsDiode.py @@ -17,27 +17,33 @@ class JlcPartsDiode(PartsTableSelectorFootprint, JlcPartsBase, TableDiode): @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: row_dict[cls.KICAD_FOOTPRINT] = JlcDiode.PACKAGE_FOOTPRINT_MAP[package] - row_dict[cls.VOLTAGE_RATING] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Reverse voltage (vr)", str), 'V')) - row_dict[cls.FORWARD_VOLTAGE] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Forward voltage (vf@if)", str).split('@')[0], 'V')) + row_dict[cls.VOLTAGE_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Reverse voltage (vr)", str), "V") + ) + row_dict[cls.FORWARD_VOLTAGE] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Forward voltage (vf@if)", str).split("@")[0], "V") + ) if "Average rectified current (io)" in attributes: - row_dict[cls.CURRENT_RATING] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Average rectified current (io)", str), 'A')) + row_dict[cls.CURRENT_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Average rectified current (io)", str), "A") + ) elif "Rectified current" in attributes: # different key for some files - row_dict[cls.CURRENT_RATING] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Rectified current", str), 'A')) + row_dict[cls.CURRENT_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Rectified current", str), "A") + ) else: raise KeyError("no current rating") try: # sometimes '-' - reverse_recovery = Range.exact(PartParserUtil.parse_value( - attributes.get("Reverse recovery time (trr)", str), 's')) + reverse_recovery = Range.exact( + PartParserUtil.parse_value(attributes.get("Reverse recovery time (trr)", str), "s") + ) except (KeyError, PartParserUtil.ParseError): if filename == "DiodesDiodes___Fast_Recovery_Rectifiers": reverse_recovery = Range(0, 500e-9) # arbitrary <500ns @@ -55,26 +61,29 @@ class JlcPartsZenerDiode(TableZenerDiode, PartsTableSelectorFootprint, JlcPartsB @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: row_dict[cls.KICAD_FOOTPRINT] = JlcDiode.PACKAGE_FOOTPRINT_MAP[package] if "Zener voltage (range)" in attributes: # note, some devices have range='-' - zener_voltage_split = attributes.get("Zener voltage (range)", str).split('~') + zener_voltage_split = attributes.get("Zener voltage (range)", str).split("~") zener_voltage = Range( - PartParserUtil.parse_value(zener_voltage_split[0], 'V'), - PartParserUtil.parse_value(zener_voltage_split[1], 'V') + PartParserUtil.parse_value(zener_voltage_split[0], "V"), + PartParserUtil.parse_value(zener_voltage_split[1], "V"), ) else: # explicit tolerance zener_voltage = PartParserUtil.parse_abs_tolerance( attributes.get("Tolerance", str), - PartParserUtil.parse_value(attributes.get("Zener voltage (nom)", str), 'V'), - '') + PartParserUtil.parse_value(attributes.get("Zener voltage (nom)", str), "V"), + "", + ) row_dict[cls.ZENER_VOLTAGE] = zener_voltage - row_dict[cls.POWER_RATING] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Power dissipation", str), 'W')) + row_dict[cls.POWER_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Power dissipation", str), "W") + ) return row_dict except (KeyError, TypeError, PartParserUtil.ParseError): diff --git a/edg/jlcparts/JlcPartsElectrolyticCapacitor.py b/edg/jlcparts/JlcPartsElectrolyticCapacitor.py index 83e849475..7e69fc642 100644 --- a/edg/jlcparts/JlcPartsElectrolyticCapacitor.py +++ b/edg/jlcparts/JlcPartsElectrolyticCapacitor.py @@ -13,21 +13,24 @@ class JlcPartsElectrolyticCapacitor(PartsTableSelectorFootprint, JlcPartsBase, T @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: match = cls._PACKAGE_PARSER.match(package) if match is None: return None - row_dict[cls.KICAD_FOOTPRINT] = f"Capacitor_SMD:CP_Elec_{match.group(1)}x{match.group(2)}", + row_dict[cls.KICAD_FOOTPRINT] = (f"Capacitor_SMD:CP_Elec_{match.group(1)}x{match.group(2)}",) try: - nominal_capacitance = attributes.get("Capacitance", float, sub='capacitance') + nominal_capacitance = attributes.get("Capacitance", float, sub="capacitance") row_dict[cls.CAPACITANCE] = PartParserUtil.parse_abs_tolerance( - attributes.get("Tolerance", str), nominal_capacitance, '') + attributes.get("Tolerance", str), nominal_capacitance, "" + ) row_dict[cls.NOMINAL_CAPACITANCE] = nominal_capacitance row_dict[cls.VOLTAGE_RATING] = Range.zero_to_upper( - attributes.get("Allowable voltage", float, sub='voltage')) + attributes.get("Allowable voltage", float, sub="voltage") + ) return row_dict except (KeyError, TypeError, PartParserUtil.ParseError): diff --git a/edg/jlcparts/JlcPartsFerriteBead.py b/edg/jlcparts/JlcPartsFerriteBead.py index d671caea0..0ec1d9e1b 100644 --- a/edg/jlcparts/JlcPartsFerriteBead.py +++ b/edg/jlcparts/JlcPartsFerriteBead.py @@ -12,24 +12,25 @@ class JlcPartsFerriteBead(PartsTableSelectorFootprint, JlcPartsBase, TableFerrit @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: row_dict[cls.KICAD_FOOTPRINT] = JlcFerriteBead.PACKAGE_FOOTPRINT_MAP[package] try: # sometimes '-' - current_rating = PartParserUtil.parse_value( - attributes.get("Current rating", str), 'A') + current_rating = PartParserUtil.parse_value(attributes.get("Current rating", str), "A") except (KeyError, PartParserUtil.ParseError): current_rating = 0 row_dict[cls.CURRENT_RATING] = Range.zero_to_upper(current_rating) - if row_dict[cls.PART_NUMBER_COL] == 'GZ2012D101TF': # additional data for a basic part - row_dict[cls.DC_RESISTANCE] = Range.exact(0.15) # from https://evelta.com/content/datasheets/212-Ferrite-2012.pdf + if row_dict[cls.PART_NUMBER_COL] == "GZ2012D101TF": # additional data for a basic part + row_dict[cls.DC_RESISTANCE] = Range.exact( + 0.15 + ) # from https://evelta.com/content/datasheets/212-Ferrite-2012.pdf else: # Dc resistance sometimes NaN - row_dict[cls.DC_RESISTANCE] = Range.exact(attributes.get("Dc resistance", float, sub='resistance')) - row_dict[cls.HF_IMPEDANCE] = Range.exact( - attributes.get("Impedance @ frequency", float, sub='esr')) + row_dict[cls.DC_RESISTANCE] = Range.exact(attributes.get("Dc resistance", float, sub="resistance")) + row_dict[cls.HF_IMPEDANCE] = Range.exact(attributes.get("Impedance @ frequency", float, sub="esr")) return row_dict except (KeyError, TypeError, PartParserUtil.ParseError): diff --git a/edg/jlcparts/JlcPartsFet.py b/edg/jlcparts/JlcPartsFet.py index 6971c6caf..c7d2ddc45 100644 --- a/edg/jlcparts/JlcPartsFet.py +++ b/edg/jlcparts/JlcPartsFet.py @@ -10,45 +10,52 @@ class JlcPartsBaseFet(JlcPartsBase, BaseTableFet): _JLC_PARTS_FILE_NAMES = ["TransistorsMOSFETs"] _CHANNEL_MAP = { - 'N Channel': 'N', - '1 N-channel': 'N', - '1 N-Channel': 'N', - 'P Channel': 'P', - '1 Piece P-Channel': 'P', + "N Channel": "N", + "1 N-channel": "N", + "1 N-Channel": "N", + "P Channel": "P", + "1 Piece P-Channel": "P", } @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: row_dict[cls.KICAD_FOOTPRINT] = JlcFet.PACKAGE_FOOTPRINT_MAP[package] row_dict[cls.CHANNEL] = cls._CHANNEL_MAP[attributes.get("Type", str)] row_dict[cls.VDS_RATING] = Range.zero_to_upper( - attributes.get("Drain source voltage (vdss)", float, sub='voltage')) - row_dict[cls.IDS_RATING] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Continuous drain current (id)", str), 'A')) + attributes.get("Drain source voltage (vdss)", float, sub="voltage") + ) + row_dict[cls.IDS_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Continuous drain current (id)", str), "A") + ) # used as a proxy for lower bound for Vgs,max - vgs_for_ids = attributes.get("Drain source on resistance (rds(on)@vgs,id)", float, sub='Vgs') + vgs_for_ids = attributes.get("Drain source on resistance (rds(on)@vgs,id)", float, sub="Vgs") row_dict[cls.VGS_RATING] = Range.from_abs_tolerance(0, vgs_for_ids) # bidirectional rating row_dict[cls.VGS_DRIVE] = Range( - attributes.get("Gate threshold voltage (vgs(th)@id)", float, sub='Vgs'), - vgs_for_ids) + attributes.get("Gate threshold voltage (vgs(th)@id)", float, sub="Vgs"), vgs_for_ids + ) row_dict[cls.RDS_ON] = Range.exact( - attributes.get("Drain source on resistance (rds(on)@vgs,id)", float, sub='Rds')) + attributes.get("Drain source on resistance (rds(on)@vgs,id)", float, sub="Rds") + ) row_dict[cls.POWER_RATING] = Range.zero_to_upper( - attributes.get("Power dissipation (pd)", float, sub='power')) + attributes.get("Power dissipation (pd)", float, sub="power") + ) try: - input_capacitance: Optional[float] = attributes.get("Input capacitance (ciss@vds)", float, sub='capacity') + input_capacitance: Optional[float] = attributes.get( + "Input capacitance (ciss@vds)", float, sub="capacity" + ) except (KeyError, TypeError): input_capacitance = None try: # not specified for most parts apparently - gate_charge: Optional[float] = attributes.get("Total gate charge (qg@vgs)", float, sub='charge') + gate_charge: Optional[float] = attributes.get("Total gate charge (qg@vgs)", float, sub="charge") except (KeyError, TypeError): if input_capacitance is not None: # not strictly correct but kind of a guesstimate gate_charge = input_capacitance * vgs_for_ids diff --git a/edg/jlcparts/JlcPartsInductor.py b/edg/jlcparts/JlcPartsInductor.py index e91151743..7ca715347 100644 --- a/edg/jlcparts/JlcPartsInductor.py +++ b/edg/jlcparts/JlcPartsInductor.py @@ -17,13 +17,16 @@ class JlcPartsInductor(PartsTableSelectorFootprint, JlcPartsBase, TableInductor) @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: # some standard sizes eg 0603 can be parsed from the package footprint = JlcInductor.PACKAGE_FOOTPRINT_MAP.get(package, None) if footprint is None: - footprint_cols = JlcInductor.parse_full_description(row_dict[cls.PART_NUMBER_COL], JlcInductor.PART_FOOTPRINT_PARSERS) + footprint_cols = JlcInductor.parse_full_description( + row_dict[cls.PART_NUMBER_COL], JlcInductor.PART_FOOTPRINT_PARSERS + ) if footprint_cols is not None: footprint = footprint_cols[cls.KICAD_FOOTPRINT] else: @@ -31,10 +34,10 @@ def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: st row_dict[cls.KICAD_FOOTPRINT] = footprint row_dict[cls.INDUCTANCE] = PartParserUtil.parse_abs_tolerance( - attributes.get("Tolerance", str), attributes.get("Inductance", float, sub='inductance'), '') - row_dict[cls.CURRENT_RATING] = Range.zero_to_upper( - attributes.get("Rated current", float, 0, sub='current')) - row_dict[cls.DC_RESISTANCE] = Range.exact(attributes.get("Dc resistance", float, 0, sub='resistance')) + attributes.get("Tolerance", str), attributes.get("Inductance", float, sub="inductance"), "" + ) + row_dict[cls.CURRENT_RATING] = Range.zero_to_upper(attributes.get("Rated current", float, 0, sub="current")) + row_dict[cls.DC_RESISTANCE] = Range.exact(attributes.get("Dc resistance", float, 0, sub="resistance")) row_dict[cls.FREQUENCY_RATING] = Range.all() # TODO ignored for now return row_dict diff --git a/edg/jlcparts/JlcPartsLed.py b/edg/jlcparts/JlcPartsLed.py index c889b9acc..72405d732 100644 --- a/edg/jlcparts/JlcPartsLed.py +++ b/edg/jlcparts/JlcPartsLed.py @@ -14,32 +14,33 @@ class JlcPartsLed(PartsTableSelectorFootprint, JlcPartsBase, TableLed): "Photoelectric_DevicesLight_Emitting_Diodes__LED_", "OptocouplerakaLEDakaDigital_TubeakaPhotoelectric_DeviceLight_Emitting_Diodes__LED_", "Optocouplers_and_LEDs_and_InfraredLight_Emitting_Diodes__LED_", - ] + ] _COLOR_MAP = { - 'red': Led.Red, - 'orange': Led.Orange, - 'amber': Led.Yellow, - 'yellow': Led.Yellow, - 'green/yellow-green': Led.GreenYellow, - 'yellow-green': Led.GreenYellow, - 'green-yellow': Led.GreenYellow, - 'emerald': Led.Green, - 'blue': Led.Blue, - 'ice blue': Led.Blue, - 'white': Led.White, - 'white light': Led.White, + "red": Led.Red, + "orange": Led.Orange, + "amber": Led.Yellow, + "yellow": Led.Yellow, + "green/yellow-green": Led.GreenYellow, + "yellow-green": Led.GreenYellow, + "green-yellow": Led.GreenYellow, + "emerald": Led.Green, + "blue": Led.Blue, + "ice blue": Led.Blue, + "white": Led.White, + "white light": Led.White, } @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: row_dict[cls.KICAD_FOOTPRINT] = JlcLed.PACKAGE_FOOTPRINT_MAP[package] part_color: Optional[str] = None - if 'Emitted color' in attributes: - table_color = attributes.get('Emitted color', str).lower() + if "Emitted color" in attributes: + table_color = attributes.get("Emitted color", str).lower() part_color = cls._COLOR_MAP[table_color] else: # older basic parts don't have the parametrics desc = row_dict[cls.DESCRIPTION_COL].lower() diff --git a/edg/jlcparts/JlcPartsMlcc.py b/edg/jlcparts/JlcPartsMlcc.py index 50618f42c..dc2248284 100644 --- a/edg/jlcparts/JlcPartsMlcc.py +++ b/edg/jlcparts/JlcPartsMlcc.py @@ -17,24 +17,27 @@ def __init__(self, *args: Any, capacitance_minimum_size: BoolLike = True, **kwar @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: footprint = JlcCapacitor.PACKAGE_FOOTPRINT_MAP[package] row_dict[cls.KICAD_FOOTPRINT] = footprint - nominal_capacitance = attributes.get("Capacitance", float, sub='capacitance') + nominal_capacitance = attributes.get("Capacitance", float, sub="capacitance") # note, tolerance not specified for many devices row_dict[cls.CAPACITANCE] = PartParserUtil.parse_abs_tolerance( - attributes.get("Tolerance", str), nominal_capacitance, '') + attributes.get("Tolerance", str), nominal_capacitance, "" + ) row_dict[cls.NOMINAL_CAPACITANCE] = nominal_capacitance row_dict[cls.VOLTAGE_RATING] = Range.from_abs_tolerance( # voltage rating for ceramic caps is bidirectional - 0, attributes.get("Allowable voltage", float, 0, sub='voltage')) + 0, attributes.get("Allowable voltage", float, 0, sub="voltage") + ) row_dict[cls.VOLTCO] = JlcCapacitor.DERATE_VOLTCO_MAP[footprint] # arbitrary filter - TODO parameterization tempco = attributes.get("Temperature coefficient", str) - if len(tempco) < 3 or tempco[0] not in ('X', 'C', 'N') or tempco[2] not in ('R', 'S', 'G', '0'): + if len(tempco) < 3 or tempco[0] not in ("X", "C", "N") or tempco[2] not in ("R", "S", "G", "0"): return None return row_dict @@ -49,15 +52,16 @@ def filter_minimum_size(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, A nominal_capacitance = row[self.NOMINAL_CAPACITANCE] footprint = row[self.KICAD_FOOTPRINT] if nominal_capacitance > 10e-6 and footprint not in [ - 'Capacitor_SMD:C_1206_3216Metric', + "Capacitor_SMD:C_1206_3216Metric", ]: return None elif nominal_capacitance > 1e-6 and footprint not in [ - 'Capacitor_SMD:C_0805_2012Metric', - 'Capacitor_SMD:C_1206_3216Metric', + "Capacitor_SMD:C_0805_2012Metric", + "Capacitor_SMD:C_1206_3216Metric", ]: return None return {} + table = super()._table_postprocess(table) if self.get(self.capacitance_minimum_size): table = table.map_new_columns(filter_minimum_size) @@ -80,13 +84,16 @@ def _row_generate(self, row: PartsTableRow) -> None: @override def _make_parallel_footprints(self, row: PartsTableRow) -> None: - cap_model = JlcDummyCapacitor(set_lcsc_part=row[self.LCSC_COL], - set_basic_part=row[self.BASIC_PART_COL], - footprint=row[self.KICAD_FOOTPRINT], - manufacturer=row[self.MANUFACTURER_COL], part_number=row[self.PART_NUMBER_COL], - value=row[self.DESCRIPTION_COL], - capacitance=row[self.NOMINAL_CAPACITANCE], - voltage=self.voltage) + cap_model = JlcDummyCapacitor( + set_lcsc_part=row[self.LCSC_COL], + set_basic_part=row[self.BASIC_PART_COL], + footprint=row[self.KICAD_FOOTPRINT], + manufacturer=row[self.MANUFACTURER_COL], + part_number=row[self.PART_NUMBER_COL], + value=row[self.DESCRIPTION_COL], + capacitance=row[self.NOMINAL_CAPACITANCE], + voltage=self.voltage, + ) self.c = ElementDict[JlcDummyCapacitor]() for i in range(row[self.PARALLEL_COUNT]): self.c[i] = self.Block(cap_model) diff --git a/edg/jlcparts/JlcPartsPptcFuse.py b/edg/jlcparts/JlcPartsPptcFuse.py index aa27d3e87..749d7f511 100644 --- a/edg/jlcparts/JlcPartsPptcFuse.py +++ b/edg/jlcparts/JlcPartsPptcFuse.py @@ -12,17 +12,21 @@ class JlcPartsPptcFuse(PartsTableSelectorFootprint, JlcPartsBase, TableFuse, Ppt @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: row_dict[cls.KICAD_FOOTPRINT] = JlcPptcFuse.PACKAGE_FOOTPRINT_MAP[package] - row_dict[cls.VOLTAGE_RATING] = Range.zero_to_upper(PartParserUtil.parse_value( - attributes.get("Operating voltage (max)", str), 'V')) - row_dict[cls.TRIP_CURRENT] = Range.exact(PartParserUtil.parse_value( - attributes.get("Trip current", str), 'A')) - row_dict[cls.HOLD_CURRENT] = Range.exact(PartParserUtil.parse_value( - attributes.get("Hold current", str), 'A')) + row_dict[cls.VOLTAGE_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(attributes.get("Operating voltage (max)", str), "V") + ) + row_dict[cls.TRIP_CURRENT] = Range.exact( + PartParserUtil.parse_value(attributes.get("Trip current", str), "A") + ) + row_dict[cls.HOLD_CURRENT] = Range.exact( + PartParserUtil.parse_value(attributes.get("Hold current", str), "A") + ) return row_dict except (KeyError, TypeError, PartParserUtil.ParseError): diff --git a/edg/jlcparts/JlcPartsResistorSmd.py b/edg/jlcparts/JlcPartsResistorSmd.py index d9968e147..849d68d3f 100644 --- a/edg/jlcparts/JlcPartsResistorSmd.py +++ b/edg/jlcparts/JlcPartsResistorSmd.py @@ -12,20 +12,20 @@ class JlcPartsResistorSmd(PartsTableSelectorFootprint, JlcPartsBase, TableResist @classmethod @override - def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes) \ - -> Optional[Dict[PartsTableColumn, Any]]: + def _entry_to_table_row( + cls, row_dict: Dict[PartsTableColumn, Any], filename: str, package: str, attributes: JlcPartsAttributes + ) -> Optional[Dict[PartsTableColumn, Any]]: try: row_dict[cls.KICAD_FOOTPRINT] = JlcResistor.PACKAGE_FOOTPRINT_MAP[package] row_dict[cls.RESISTANCE] = PartParserUtil.parse_abs_tolerance( - attributes.get("Tolerance", str), attributes.get("Resistance", float, sub='resistance'), '') - - row_dict[cls.POWER_RATING] = Range.zero_to_upper( - attributes.get("Power", float, 0, sub='power') + attributes.get("Tolerance", str), attributes.get("Resistance", float, sub="resistance"), "" ) + row_dict[cls.POWER_RATING] = Range.zero_to_upper(attributes.get("Power", float, 0, sub="power")) + try: # sometimes '-' - voltage_rating = PartParserUtil.parse_value(attributes.get("Overload voltage (max)", str), 'V') + voltage_rating = PartParserUtil.parse_value(attributes.get("Overload voltage (max)", str), "V") except (KeyError, PartParserUtil.ParseError): voltage_rating = 0 row_dict[cls.VOLTAGE_RATING] = Range.zero_to_upper(voltage_rating) diff --git a/edg/parts/AdcSpi_Mcp3201.py b/edg/parts/AdcSpi_Mcp3201.py index 615fe3a80..422b63084 100644 --- a/edg/parts/AdcSpi_Mcp3201.py +++ b/edg/parts/AdcSpi_Mcp3201.py @@ -4,78 +4,84 @@ class Mcp3201_Device(InternalSubcircuit, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, - current_draw=(0.5, 400)*uAmp)) # from standby to operating - self.vss = self.Port(Ground()) + def __init__(self) -> None: + super().__init__() + self.vdd = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(0.5, 400) * uAmp) + ) # from standby to operating + self.vss = self.Port(Ground()) - self.vref = self.Port(VoltageSink( - voltage_limits=(0.25*Volt, self.vdd.link().voltage.upper()), - current_draw=(0.001, 150)*uAmp - )) - self.inp = self.Port(AnalogSink.from_supply( - self.vss, self.vref, - voltage_limit_tolerance=(-0.6, 0.6)*Volt, - signal_limit_tolerance=(-0.1, 0.1)*Volt, # kinda, actually from IN- to Vref+IN-, for IN- within 0.1V of Vss - impedance=(5, 5000)*MOhm # derived from assumption Vin=5 / 0.001 - 1uA leakage current - )) + self.vref = self.Port( + VoltageSink(voltage_limits=(0.25 * Volt, self.vdd.link().voltage.upper()), current_draw=(0.001, 150) * uAmp) + ) + self.inp = self.Port( + AnalogSink.from_supply( + self.vss, + self.vref, + voltage_limit_tolerance=(-0.6, 0.6) * Volt, + signal_limit_tolerance=(-0.1, 0.1) + * Volt, # kinda, actually from IN- to Vref+IN-, for IN- within 0.1V of Vss + impedance=(5, 5000) * MOhm, # derived from assumption Vin=5 / 0.001 - 1uA leakage current + ) + ) - dio_model = DigitalBidir.from_supply( - self.vss, self.vdd, - voltage_limit_tolerance=(-0.6, 0.6)*Volt, - input_threshold_factor=(0.3, 0.7) - ) - # Datasheet section 6.2, minimum clock speed - self.spi = self.Port(SpiPeripheral(dio_model, frequency_limit=(10, 1600) * kHertz)) - self.cs = self.Port(dio_model) + dio_model = DigitalBidir.from_supply( + self.vss, self.vdd, voltage_limit_tolerance=(-0.6, 0.6) * Volt, input_threshold_factor=(0.3, 0.7) + ) + # Datasheet section 6.2, minimum clock speed + self.spi = self.Port(SpiPeripheral(dio_model, frequency_limit=(10, 1600) * kHertz)) + self.cs = self.Port(dio_model) - @override - def contents(self) -> None: - # Note, B-grade chip has lower INL (+/-1 LSB) compared to C-grade (+/-2 LSB) - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.vref, - '2': self.inp, - '3': self.vss, # IN-, but because it's so limited this is configured for single-ended mode - '4': self.vss, - '5': self.cs, - '6': self.spi.miso, - '7': self.spi.sck, - '8': self.vdd, - }, - mfr='Microchip Technology', part='MCP3201T-BI/SN', - datasheet='https://ww1.microchip.com/downloads/en/DeviceDoc/21290F.pdf' - ) + @override + def contents(self) -> None: + # Note, B-grade chip has lower INL (+/-1 LSB) compared to C-grade (+/-2 LSB) + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.vref, + "2": self.inp, + "3": self.vss, # IN-, but because it's so limited this is configured for single-ended mode + "4": self.vss, + "5": self.cs, + "6": self.spi.miso, + "7": self.spi.sck, + "8": self.vdd, + }, + mfr="Microchip Technology", + part="MCP3201T-BI/SN", + datasheet="https://ww1.microchip.com/downloads/en/DeviceDoc/21290F.pdf", + ) class Mcp3201(AnalogToDigital, Block): - """MCP3201 12-bit 100kSPS ADC configured in single-ended mode, since the IN- pin can't do much anyways. + """MCP3201 12-bit 100kSPS ADC configured in single-ended mode, since the IN- pin can't do much anyways. - Some drop-in electrically compatible chips: - - ADS7822 (12 bit, 200kSPS) - - MCP3551 (22 bit, low sample rate, delta-sigma) - - SLIGHTLY DIFFERENT PINNING! SCK and CS swapped! - """ - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Mcp3201_Device()) - self.pwr = self.Export(self.ic.vdd, [Power]) - self.gnd = self.Export(self.ic.vss, [Common]) + Some drop-in electrically compatible chips: + - ADS7822 (12 bit, 200kSPS) + - MCP3551 (22 bit, low sample rate, delta-sigma) + - SLIGHTLY DIFFERENT PINNING! SCK and CS swapped! + """ - self.ref = self.Export(self.ic.vref) - self.vin = self.Export(self.ic.inp, [Input]) + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Mcp3201_Device()) + self.pwr = self.Export(self.ic.vdd, [Power]) + self.gnd = self.Export(self.ic.vss, [Common]) - self.spi = self.Export(self.ic.spi, [Output]) - self.cs = self.Export(self.ic.cs) + self.ref = self.Export(self.ic.vref) + self.vin = self.Export(self.ic.inp, [Input]) - @override - def contents(self) -> None: - super().contents() + self.spi = self.Export(self.ic.spi, [Output]) + self.cs = self.Export(self.ic.cs) - # Datasheet Section 6.4: 1uF cap recommended - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + @override + def contents(self) -> None: + super().contents() + + # Datasheet Section 6.4: 1uF cap recommended + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) diff --git a/edg/parts/AdcSpi_Mcp3561.py b/edg/parts/AdcSpi_Mcp3561.py index 35d90e4b0..5cb16b67c 100644 --- a/edg/parts/AdcSpi_Mcp3561.py +++ b/edg/parts/AdcSpi_Mcp3561.py @@ -6,145 +6,148 @@ class Mcp3561_Device(InternalSubcircuit, GeneratorBlock, FootprintBlock): - def __init__(self, has_ext_ref: BoolLike) -> None: - super().__init__() - self.avdd = self.Port(VoltageSink( - voltage_limits=(2.7, 3.6)*Volt, - current_draw=(0.0004, 2.5)*mAmp)) # shutdown to max operating currrent - self.dvdd = self.Port(VoltageSink( - voltage_limits=(1.8*Volt, self.avdd.link().voltage.upper() + 0.1), - current_draw=(0.002, 0.37)*mAmp)) # shutdown to max operating current - self.vss = self.Port(Ground()) - - self.vrefp = self.Port(VoltageSink( # TODO also operates in output configuration, up to 2.4v 2% - voltage_limits=(0.6*Volt, self.avdd.link().voltage.upper()), - )) # non-optional, requires decoupling cap - - avdd_range = self.vss.link().voltage.hull(self.avdd.link().voltage) - vrefp_range = self.vss.link().voltage.hull(self.avdd.link().voltage) - input_model = AnalogSink.from_supply( - self.vss, self.avdd, # maximum ratings up to AVdd - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - signal_limit_abs=(vrefp_range * 3).intersect(avdd_range), # support GAIN=0.33 - impedance=(20, 510)*kOhm # varies based on gain - ) - self.ch = self.Port(Vector(AnalogSink.empty())) - for i in range(8): - self.ch.append_elt(input_model, str(i)) - - dio_model = DigitalBidir.from_supply( - self.vss, self.dvdd, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - input_threshold_factor=(0.3, 0.7) - ) - # Datasheet table 1.1 - self.spi = self.Port(SpiPeripheral(dio_model, frequency_limit=(0, 10) * MHertz)) # note 20MHz for >2.7V DVdd - self.cs = self.Port(dio_model) - self.mclkin = self.Port(DigitalSink.from_bidir(dio_model), optional=True) # clkin on TSSOP devices - - self.has_ext_ref = self.ArgParameter(has_ext_ref) - self.generator_param(self.ch.requested(), self.vrefp.is_connected(), self.has_ext_ref) - - @override - def generate(self) -> None: - ch_requested = self.get(self.ch.requested()) - CHANNEL_USE_MAPPINGS = [ - (('7', '6', '5', '4'), 'MCP3564'), - (('3', '2'), 'MCP3562'), - (('0', '1'), 'MCP3561'), - ] - part: Optional[str] = None - for (used_channels, used_part) in CHANNEL_USE_MAPPINGS: - for used_channel in used_channels: - if used_channel in ch_requested: - part = used_part - break - if part: - break - assert part is not None, 'no channels used' - - if not self.get(self.has_ext_ref): - part = part + 'R' # if internal reference needed - - self.footprint( - 'U', 'Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm', - { - '1': self.avdd, - '2': self.vss, - '3': self.vss, # actually Vref- - '4': self.vrefp, - '5': self.ch['0'], - '6': self.ch['1'], - '7': self.ch['2'], - '8': self.ch['3'], - '9': self.ch['4'], - '10': self.ch['5'], - '11': self.ch['6'], - '12': self.ch['7'], - '13': self.cs, - '14': self.spi.sck, - '15': self.spi.mosi, - '16': self.spi.miso, - # '17': nIRQ / MDAT - '18': self.mclkin, - '19': self.vss, - '20': self.dvdd, - }, - mfr='Microchip Technology', part=part + '-*/ST', - datasheet='https://ww1.microchip.com/downloads/en/DeviceDoc/MCP3561.2.4R-Data-Sheet-DS200006391A.pdf' - ) + def __init__(self, has_ext_ref: BoolLike) -> None: + super().__init__() + self.avdd = self.Port( + VoltageSink(voltage_limits=(2.7, 3.6) * Volt, current_draw=(0.0004, 2.5) * mAmp) + ) # shutdown to max operating currrent + self.dvdd = self.Port( + VoltageSink( + voltage_limits=(1.8 * Volt, self.avdd.link().voltage.upper() + 0.1), current_draw=(0.002, 0.37) * mAmp + ) + ) # shutdown to max operating current + self.vss = self.Port(Ground()) + + self.vrefp = self.Port( + VoltageSink( # TODO also operates in output configuration, up to 2.4v 2% + voltage_limits=(0.6 * Volt, self.avdd.link().voltage.upper()), + ) + ) # non-optional, requires decoupling cap + + avdd_range = self.vss.link().voltage.hull(self.avdd.link().voltage) + vrefp_range = self.vss.link().voltage.hull(self.avdd.link().voltage) + input_model = AnalogSink.from_supply( + self.vss, + self.avdd, # maximum ratings up to AVdd + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + signal_limit_abs=(vrefp_range * 3).intersect(avdd_range), # support GAIN=0.33 + impedance=(20, 510) * kOhm, # varies based on gain + ) + self.ch = self.Port(Vector(AnalogSink.empty())) + for i in range(8): + self.ch.append_elt(input_model, str(i)) + + dio_model = DigitalBidir.from_supply( + self.vss, self.dvdd, voltage_limit_tolerance=(-0.3, 0.3) * Volt, input_threshold_factor=(0.3, 0.7) + ) + # Datasheet table 1.1 + self.spi = self.Port(SpiPeripheral(dio_model, frequency_limit=(0, 10) * MHertz)) # note 20MHz for >2.7V DVdd + self.cs = self.Port(dio_model) + self.mclkin = self.Port(DigitalSink.from_bidir(dio_model), optional=True) # clkin on TSSOP devices + + self.has_ext_ref = self.ArgParameter(has_ext_ref) + self.generator_param(self.ch.requested(), self.vrefp.is_connected(), self.has_ext_ref) + + @override + def generate(self) -> None: + ch_requested = self.get(self.ch.requested()) + CHANNEL_USE_MAPPINGS = [ + (("7", "6", "5", "4"), "MCP3564"), + (("3", "2"), "MCP3562"), + (("0", "1"), "MCP3561"), + ] + part: Optional[str] = None + for used_channels, used_part in CHANNEL_USE_MAPPINGS: + for used_channel in used_channels: + if used_channel in ch_requested: + part = used_part + break + if part: + break + assert part is not None, "no channels used" + + if not self.get(self.has_ext_ref): + part = part + "R" # if internal reference needed + + self.footprint( + "U", + "Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm", + { + "1": self.avdd, + "2": self.vss, + "3": self.vss, # actually Vref- + "4": self.vrefp, + "5": self.ch["0"], + "6": self.ch["1"], + "7": self.ch["2"], + "8": self.ch["3"], + "9": self.ch["4"], + "10": self.ch["5"], + "11": self.ch["6"], + "12": self.ch["7"], + "13": self.cs, + "14": self.spi.sck, + "15": self.spi.mosi, + "16": self.spi.miso, + # '17': nIRQ / MDAT + "18": self.mclkin, + "19": self.vss, + "20": self.dvdd, + }, + mfr="Microchip Technology", + part=part + "-*/ST", + datasheet="https://ww1.microchip.com/downloads/en/DeviceDoc/MCP3561.2.4R-Data-Sheet-DS200006391A.pdf", + ) class Mcp3561(AnalogToDigital, GeneratorBlock): - """MCP3561R up-to-24-bit delta-sigma ADC with internal voltage reference. - IMPORTANT - an antialias filter is REQUIRED at the inputs. The reference design uses a RC with 1k and 0.1uF (fc=10kHz) - with the general recommendation being low R and high C and with low time constant to provide high rejection at DMCLK. - TODO: assert that an antialias filter is connected - """ - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Mcp3561_Device(BoolExpr())) - self.pwra = self.Port(VoltageSink.empty()) - self.pwr = self.Port(VoltageSink.empty()) - self.gnd = self.Export(self.ic.vss, [Common]) - self.vref = self.Port(VoltageSink.empty(), optional=True) - self.mclkin = self.Export(self.ic.mclkin, optional=True) - self.assign(self.ic.has_ext_ref, self.vref.is_connected()) - self.generator_param(self.vref.is_connected()) - - self.vins = self.Export(self.ic.ch) - - self.spi = self.Export(self.ic.spi, [Output]) - self.cs = self.Export(self.ic.cs) - - @override - def contents(self) -> None: - super().contents() - - self.avdd_res = self.Block(SeriesPowerResistor( - 10*Ohm(tol=0.05) - )).connected(self.pwra, self.ic.avdd) - self.dvdd_res = self.Block(SeriesPowerResistor( - 10*Ohm(tol=0.05) - )).connected(self.pwr, self.ic.dvdd) - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - cap_model = DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2)) - self.avdd_cap_0 = imp.Block(cap_model).connected(pwr=self.ic.avdd) - self.avdd_cap_1 = imp.Block(cap_model).connected(pwr=self.ic.avdd) - self.dvdd_cap_0 = imp.Block(cap_model).connected(pwr=self.ic.dvdd) - self.dvdd_cap_1 = imp.Block(cap_model).connected(pwr=self.ic.dvdd) - - # technically optional, but accuracy potentially degraded if omitted - self.vref_cap = imp.Block(DecouplingCapacitor(10*uFarad(tol=0.2))).connected(pwr=self.ic.vrefp) - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.vref.is_connected()): - self.connect(self.vref, self.ic.vrefp) - else: # dummy source, for the Vref capacitor - (self.vrefp_source, ), _ = self.chain(self.Block(DummyVoltageSource(voltage_out=2.4*Volt(tol=0.02))), - self.ic.vrefp) + """MCP3561R up-to-24-bit delta-sigma ADC with internal voltage reference. + IMPORTANT - an antialias filter is REQUIRED at the inputs. The reference design uses a RC with 1k and 0.1uF (fc=10kHz) + with the general recommendation being low R and high C and with low time constant to provide high rejection at DMCLK. + TODO: assert that an antialias filter is connected + """ + + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Mcp3561_Device(BoolExpr())) + self.pwra = self.Port(VoltageSink.empty()) + self.pwr = self.Port(VoltageSink.empty()) + self.gnd = self.Export(self.ic.vss, [Common]) + self.vref = self.Port(VoltageSink.empty(), optional=True) + self.mclkin = self.Export(self.ic.mclkin, optional=True) + self.assign(self.ic.has_ext_ref, self.vref.is_connected()) + self.generator_param(self.vref.is_connected()) + + self.vins = self.Export(self.ic.ch) + + self.spi = self.Export(self.ic.spi, [Output]) + self.cs = self.Export(self.ic.cs) + + @override + def contents(self) -> None: + super().contents() + + self.avdd_res = self.Block(SeriesPowerResistor(10 * Ohm(tol=0.05))).connected(self.pwra, self.ic.avdd) + self.dvdd_res = self.Block(SeriesPowerResistor(10 * Ohm(tol=0.05))).connected(self.pwr, self.ic.dvdd) + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + cap_model = DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2)) + self.avdd_cap_0 = imp.Block(cap_model).connected(pwr=self.ic.avdd) + self.avdd_cap_1 = imp.Block(cap_model).connected(pwr=self.ic.avdd) + self.dvdd_cap_0 = imp.Block(cap_model).connected(pwr=self.ic.dvdd) + self.dvdd_cap_1 = imp.Block(cap_model).connected(pwr=self.ic.dvdd) + + # technically optional, but accuracy potentially degraded if omitted + self.vref_cap = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(pwr=self.ic.vrefp) + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.vref.is_connected()): + self.connect(self.vref, self.ic.vrefp) + else: # dummy source, for the Vref capacitor + (self.vrefp_source,), _ = self.chain( + self.Block(DummyVoltageSource(voltage_out=2.4 * Volt(tol=0.02))), self.ic.vrefp + ) diff --git a/edg/parts/AnalogSwitch_7400.py b/edg/parts/AnalogSwitch_7400.py index 76e9d066b..157794c8f 100644 --- a/edg/parts/AnalogSwitch_7400.py +++ b/edg/parts/AnalogSwitch_7400.py @@ -5,75 +5,84 @@ class Sn74lvc1g3157_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - - self.gnd = self.Port(Ground()) - self.vcc = self.Port(VoltageSink( - voltage_limits=(1.65, 5.5)*Volt, - current_draw=(1, 35)*uAmp, # Icc, at 5.5v - )) - - self.s = self.Port(DigitalSink.from_supply(self.gnd, self.vcc, - voltage_limit_abs=(-0.5, 6)*Volt, - current_draw=(-1, 1)*uAmp, # input leakage current - input_threshold_factor=(0.25, 0.75) - )) - - self.analog_voltage_limits = self.Parameter(RangeExpr(( - self.gnd.link().voltage.upper() - 0.5, - self.vcc.link().voltage.lower() + 0.5 - ))) - self.analog_current_limits = self.Parameter(RangeExpr((-128, 128)*mAmp)) - self.analog_on_resistance = self.Parameter(RangeExpr((6, 30)*Ohm)) # typ-max, across all conditions - - self.a = self.Port(Passive()) - self.b1 = self.Port(Passive(), optional=True) - self.b0 = self.Port(Passive(), optional=True) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-363_SC-70-6', - { - '1': self.b1, - '2': self.gnd, - '3': self.b0, - '4': self.a, - '5': self.vcc, - '6': self.s, - }, - mfr='Texas Instruments', part='SN74LVC1G3157DCKR', - datasheet='https://www.ti.com/lit/ds/symlink/sn74lvc1g3157.pdf' - ) - self.assign(self.lcsc_part, 'C38663') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + + self.gnd = self.Port(Ground()) + self.vcc = self.Port( + VoltageSink( + voltage_limits=(1.65, 5.5) * Volt, + current_draw=(1, 35) * uAmp, # Icc, at 5.5v + ) + ) + + self.s = self.Port( + DigitalSink.from_supply( + self.gnd, + self.vcc, + voltage_limit_abs=(-0.5, 6) * Volt, + current_draw=(-1, 1) * uAmp, # input leakage current + input_threshold_factor=(0.25, 0.75), + ) + ) + + self.analog_voltage_limits = self.Parameter( + RangeExpr((self.gnd.link().voltage.upper() - 0.5, self.vcc.link().voltage.lower() + 0.5)) + ) + self.analog_current_limits = self.Parameter(RangeExpr((-128, 128) * mAmp)) + self.analog_on_resistance = self.Parameter(RangeExpr((6, 30) * Ohm)) # typ-max, across all conditions + + self.a = self.Port(Passive()) + self.b1 = self.Port(Passive(), optional=True) + self.b0 = self.Port(Passive(), optional=True) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-363_SC-70-6", + { + "1": self.b1, + "2": self.gnd, + "3": self.b0, + "4": self.a, + "5": self.vcc, + "6": self.s, + }, + mfr="Texas Instruments", + part="SN74LVC1G3157DCKR", + datasheet="https://www.ti.com/lit/ds/symlink/sn74lvc1g3157.pdf", + ) + self.assign(self.lcsc_part, "C38663") + self.assign(self.actual_basic_part, False) class Sn74lvc1g3157(AnalogSwitch): - """2:1 analog switch, 6ohm Ron(typ), in SOT-363. - """ - @override - def contents(self) -> None: - super().contents() - - self.require(~self.control_gnd.is_connected(), "device does not support control ground") - - self.ic = self.Block(Sn74lvc1g3157_Device()) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.gnd) - self.connect(self.com, self.ic.a) - self.connect(self.inputs.append_elt(Passive.empty(), '0'), self.ic.b0) - self.connect(self.inputs.append_elt(Passive.empty(), '1'), self.ic.b1) - self.connect(self.control.append_elt(DigitalSink.empty(), '0'), self.ic.s) - - self.assign(self.analog_voltage_limits, self.ic.analog_voltage_limits) - self.assign(self.analog_current_limits, self.ic.analog_current_limits) - self.assign(self.analog_on_resistance, self.ic.analog_on_resistance) - - # surprisingly, the datasheet doesn't actually specify any decoupling capacitors, but here's one anyways - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + """2:1 analog switch, 6ohm Ron(typ), in SOT-363.""" + + @override + def contents(self) -> None: + super().contents() + + self.require(~self.control_gnd.is_connected(), "device does not support control ground") + + self.ic = self.Block(Sn74lvc1g3157_Device()) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.gnd) + self.connect(self.com, self.ic.a) + self.connect(self.inputs.append_elt(Passive.empty(), "0"), self.ic.b0) + self.connect(self.inputs.append_elt(Passive.empty(), "1"), self.ic.b1) + self.connect(self.control.append_elt(DigitalSink.empty(), "0"), self.ic.s) + + self.assign(self.analog_voltage_limits, self.ic.analog_voltage_limits) + self.assign(self.analog_current_limits, self.ic.analog_current_limits) + self.assign(self.analog_on_resistance, self.ic.analog_on_resistance) + + # surprisingly, the datasheet doesn't actually specify any decoupling capacitors, but here's one anyways + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) diff --git a/edg/parts/AnalogSwitch_Dg468.py b/edg/parts/AnalogSwitch_Dg468.py index 53e3f55d2..d9f383076 100644 --- a/edg/parts/AnalogSwitch_Dg468.py +++ b/edg/parts/AnalogSwitch_Dg468.py @@ -5,83 +5,90 @@ class Dg468_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - - self.vn = self.Port(Ground()) - self.vp = self.Port(VoltageSink( - voltage_limits=(7, 36)*Volt, # 44v abs max - current_draw=(5, 20)*uAmp, - )) - - self.gnd = self.Port(Ground()) # for control signal - self.in_ = self.Port(DigitalSink.from_supply( - self.vn, self.vp, - voltage_limit_tolerance=(-2, 2)*Volt, # or 30mA - input_threshold_abs=(0.8, 2.5)*Volt - )) - - self.analog_voltage_limits = self.Parameter(RangeExpr( - self.vp.link().voltage.hull(self.gnd.link().voltage) - )) - self.analog_current_limits = self.Parameter(RangeExpr((-30, 30)*mAmp)) # max current any pin - self.analog_on_resistance = self.Parameter(RangeExpr((7, 20)*Ohm)) # typ to max - - self.com = self.Port(Passive()) - self.no = self.Port(Passive(), optional=True) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', - { - '1': self.no, - '2': self.vn, - '3': self.in_, - '4': self.gnd, - '5': self.vp, - '6': self.com, - }, - mfr='Vishay Siliconix', part='DG468DV', - datasheet='https://www.vishay.com/docs/74413/dg467.pdf' - ) - self.assign(self.lcsc_part, 'C2651906') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + + self.vn = self.Port(Ground()) + self.vp = self.Port( + VoltageSink( + voltage_limits=(7, 36) * Volt, # 44v abs max + current_draw=(5, 20) * uAmp, + ) + ) + + self.gnd = self.Port(Ground()) # for control signal + self.in_ = self.Port( + DigitalSink.from_supply( + self.vn, + self.vp, + voltage_limit_tolerance=(-2, 2) * Volt, # or 30mA + input_threshold_abs=(0.8, 2.5) * Volt, + ) + ) + + self.analog_voltage_limits = self.Parameter(RangeExpr(self.vp.link().voltage.hull(self.gnd.link().voltage))) + self.analog_current_limits = self.Parameter(RangeExpr((-30, 30) * mAmp)) # max current any pin + self.analog_on_resistance = self.Parameter(RangeExpr((7, 20) * Ohm)) # typ to max + + self.com = self.Port(Passive()) + self.no = self.Port(Passive(), optional=True) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-6", + { + "1": self.no, + "2": self.vn, + "3": self.in_, + "4": self.gnd, + "5": self.vp, + "6": self.com, + }, + mfr="Vishay Siliconix", + part="DG468DV", + datasheet="https://www.vishay.com/docs/74413/dg467.pdf", + ) + self.assign(self.lcsc_part, "C2651906") + self.assign(self.actual_basic_part, False) class Dg468(AnalogSwitch, GeneratorBlock): - """DG468 36V 10ohm SPST switch in normally-open configuration - """ - def __init__(self) -> None: - super().__init__() - self.generator_param(self.control_gnd.is_connected()) - - @override - def contents(self) -> None: - super().contents() - - self.ic = self.Block(Dg468_Device()) - self.connect(self.pwr, self.ic.vp) - self.connect(self.gnd, self.ic.vn) - self.connect(self.com, self.ic.com) - self.connect(self.inputs.append_elt(Passive.empty(), '1'), self.ic.no) - self.connect(self.control.append_elt(DigitalSink.empty(), '0'), self.ic.in_) - - self.assign(self.analog_voltage_limits, self.ic.analog_voltage_limits) - self.assign(self.analog_current_limits, self.ic.analog_current_limits) - self.assign(self.analog_on_resistance, self.ic.analog_on_resistance) - - # surprisingly, the datasheet doesn't actually specify any decoupling capacitors, but here's one anyways - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) - - @override - def generate(self) -> None: - super().generate() - if self.get(self.control_gnd.is_connected()): - self.connect(self.control_gnd, self.ic.gnd) - else: - self.connect(self.gnd, self.ic.gnd) # default + """DG468 36V 10ohm SPST switch in normally-open configuration""" + + def __init__(self) -> None: + super().__init__() + self.generator_param(self.control_gnd.is_connected()) + + @override + def contents(self) -> None: + super().contents() + + self.ic = self.Block(Dg468_Device()) + self.connect(self.pwr, self.ic.vp) + self.connect(self.gnd, self.ic.vn) + self.connect(self.com, self.ic.com) + self.connect(self.inputs.append_elt(Passive.empty(), "1"), self.ic.no) + self.connect(self.control.append_elt(DigitalSink.empty(), "0"), self.ic.in_) + + self.assign(self.analog_voltage_limits, self.ic.analog_voltage_limits) + self.assign(self.analog_current_limits, self.ic.analog_current_limits) + self.assign(self.analog_on_resistance, self.ic.analog_on_resistance) + + # surprisingly, the datasheet doesn't actually specify any decoupling capacitors, but here's one anyways + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) + + @override + def generate(self) -> None: + super().generate() + if self.get(self.control_gnd.is_connected()): + self.connect(self.control_gnd, self.ic.gnd) + else: + self.connect(self.gnd, self.ic.gnd) # default diff --git a/edg/parts/AnalogSwitch_Nlas4157.py b/edg/parts/AnalogSwitch_Nlas4157.py index c4f7e042c..a86d48fc7 100644 --- a/edg/parts/AnalogSwitch_Nlas4157.py +++ b/edg/parts/AnalogSwitch_Nlas4157.py @@ -6,79 +6,87 @@ class Nlas4157_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - - self.vcc = self.Port(VoltageSink( - voltage_limits=(1.65, 5.5)*Volt, - current_draw=(0.5, 1.0)*uAmp, # Icc, at 5.5v, typ to max - )) - self.gnd = self.Port(Ground()) - - self.s = self.Port(DigitalSink( - voltage_limits=(-0.5, 6)*Volt, - current_draw=(-1, 1)*uAmp, # input leakage current - input_thresholds=(0.6, 2.4)*Volt, # strictest of Vdd=2.7, 4.5 V - )) - - self.analog_voltage_limits = self.Parameter(RangeExpr(( - self.gnd.link().voltage.upper() - 0.5, - self.vcc.link().voltage.lower() + 0.5 - ))) - self.analog_current_limits = self.Parameter(RangeExpr((-300, 300)*mAmp)) - self.analog_on_resistance = self.Parameter(RangeExpr((0.3, 4.3)*Ohm)) - - self.a = self.Port(Passive()) - self.b1 = self.Port(Passive(), optional=True) - self.b0 = self.Port(Passive(), optional=True) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-363_SC-70-6', - { - '1': self.b1, - '2': self.gnd, - '3': self.b0, - '4': self.a, - '5': self.vcc, - '6': self.s, - }, - mfr='ON Semiconductor', part='NLAS4157', - datasheet='https://www.onsemi.com/pdf/datasheet/nlas4157-d.pdf' - ) - self.assign(self.lcsc_part, 'C106912') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + + self.vcc = self.Port( + VoltageSink( + voltage_limits=(1.65, 5.5) * Volt, + current_draw=(0.5, 1.0) * uAmp, # Icc, at 5.5v, typ to max + ) + ) + self.gnd = self.Port(Ground()) + + self.s = self.Port( + DigitalSink( + voltage_limits=(-0.5, 6) * Volt, + current_draw=(-1, 1) * uAmp, # input leakage current + input_thresholds=(0.6, 2.4) * Volt, # strictest of Vdd=2.7, 4.5 V + ) + ) + + self.analog_voltage_limits = self.Parameter( + RangeExpr((self.gnd.link().voltage.upper() - 0.5, self.vcc.link().voltage.lower() + 0.5)) + ) + self.analog_current_limits = self.Parameter(RangeExpr((-300, 300) * mAmp)) + self.analog_on_resistance = self.Parameter(RangeExpr((0.3, 4.3) * Ohm)) + + self.a = self.Port(Passive()) + self.b1 = self.Port(Passive(), optional=True) + self.b0 = self.Port(Passive(), optional=True) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-363_SC-70-6", + { + "1": self.b1, + "2": self.gnd, + "3": self.b0, + "4": self.a, + "5": self.vcc, + "6": self.s, + }, + mfr="ON Semiconductor", + part="NLAS4157", + datasheet="https://www.onsemi.com/pdf/datasheet/nlas4157-d.pdf", + ) + self.assign(self.lcsc_part, "C106912") + self.assign(self.actual_basic_part, False) @deprecated("obsolete part") class Nlas4157(AnalogSwitch): - """NLAS4157 2:1 analog switch, 1ohm Ron, in SOT-363. OBSOLETE. - Pin compatible with: - - TS5A3159: 5v tolerant, 1 ohm - - TS5A3160: 5v tolerant, 1 ohm - """ - @override - def contents(self) -> None: - super().contents() - - self.require(~self.control_gnd.is_connected(), "device does not support control ground") - - self.ic = self.Block(Nlas4157_Device()) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.gnd) - self.connect(self.com, self.ic.a) - self.connect(self.inputs.append_elt(Passive.empty(), '0'), self.ic.b0) - self.connect(self.inputs.append_elt(Passive.empty(), '1'), self.ic.b1) - self.connect(self.control.append_elt(DigitalSink.empty(), '0'), self.ic.s) - - self.assign(self.analog_voltage_limits, self.ic.analog_voltage_limits) - self.assign(self.analog_current_limits, self.ic.analog_current_limits) - self.assign(self.analog_on_resistance, self.ic.analog_on_resistance) - - # surprisingly, the datasheet doesn't actually specify any decoupling capacitors, but here's one anyways - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + """NLAS4157 2:1 analog switch, 1ohm Ron, in SOT-363. OBSOLETE. + Pin compatible with: + - TS5A3159: 5v tolerant, 1 ohm + - TS5A3160: 5v tolerant, 1 ohm + """ + + @override + def contents(self) -> None: + super().contents() + + self.require(~self.control_gnd.is_connected(), "device does not support control ground") + + self.ic = self.Block(Nlas4157_Device()) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.gnd) + self.connect(self.com, self.ic.a) + self.connect(self.inputs.append_elt(Passive.empty(), "0"), self.ic.b0) + self.connect(self.inputs.append_elt(Passive.empty(), "1"), self.ic.b1) + self.connect(self.control.append_elt(DigitalSink.empty(), "0"), self.ic.s) + + self.assign(self.analog_voltage_limits, self.ic.analog_voltage_limits) + self.assign(self.analog_current_limits, self.ic.analog_current_limits) + self.assign(self.analog_on_resistance, self.ic.analog_on_resistance) + + # surprisingly, the datasheet doesn't actually specify any decoupling capacitors, but here's one anyways + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) diff --git a/edg/parts/Batteries.py b/edg/parts/Batteries.py index ec7bdc620..674b3d4a6 100644 --- a/edg/parts/Batteries.py +++ b/edg/parts/Batteries.py @@ -6,87 +6,117 @@ class Cr2032(Battery, FootprintBlock): - def __init__(self, voltage: RangeLike = (2.0, 3.0)*Volt, *args: Any, - actual_voltage: RangeLike = (2.0, 3.0)*Volt, **kwargs: Any) -> None: - super().__init__(voltage, *args, **kwargs) - self.pwr.init_from(VoltageSource( - voltage_out=self.gnd.link().voltage + actual_voltage, # arbitrary from https://www.mouser.com/catalog/additional/Adafruit_3262.pdf - current_limits=(0, 10)*mAmp, - )) - self.gnd.init_from(Ground()) - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.actual_capacity, (210, 210)*mAmp) # TODO bounds of a few CR2032 cells; should be A*h - - self.footprint( - 'U', 'Battery:BatteryHolder_Keystone_106_1x20mm', - { - '1': self.pwr, - '2': self.gnd, - }, - mfr='Keystone', part='106' - ) + def __init__( + self, + voltage: RangeLike = (2.0, 3.0) * Volt, + *args: Any, + actual_voltage: RangeLike = (2.0, 3.0) * Volt, + **kwargs: Any, + ) -> None: + super().__init__(voltage, *args, **kwargs) + self.pwr.init_from( + VoltageSource( + voltage_out=self.gnd.link().voltage + + actual_voltage, # arbitrary from https://www.mouser.com/catalog/additional/Adafruit_3262.pdf + current_limits=(0, 10) * mAmp, + ) + ) + self.gnd.init_from(Ground()) + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.actual_capacity, (210, 210) * mAmp) # TODO bounds of a few CR2032 cells; should be A*h + + self.footprint( + "U", + "Battery:BatteryHolder_Keystone_106_1x20mm", + { + "1": self.pwr, + "2": self.gnd, + }, + mfr="Keystone", + part="106", + ) class Li18650(Battery, FootprintBlock): - def __init__(self, voltage: RangeLike = (2.5, 4.2)*Volt, *args: Any, - actual_voltage: RangeLike = (2.5, 4.2)*Volt, **kwargs: Any) -> None: - super().__init__(voltage, *args, **kwargs) - self.pwr.init_from(VoltageSource( - voltage_out=self.gnd.link().voltage + actual_voltage, - current_limits=(0, 2)*Amp, # arbitrary assuming low capacity, 1 C discharge - )) - self.gnd.init_from(Ground()) - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.actual_capacity, (2, 3.6)*Amp) # TODO should be A*h - - self.footprint( - 'U', 'Battery:BatteryHolder_Keystone_1042_1x18650', - { - '1': self.pwr, - '2': self.gnd, - }, - mfr='Keystone', part='1042' - ) + def __init__( + self, + voltage: RangeLike = (2.5, 4.2) * Volt, + *args: Any, + actual_voltage: RangeLike = (2.5, 4.2) * Volt, + **kwargs: Any, + ) -> None: + super().__init__(voltage, *args, **kwargs) + self.pwr.init_from( + VoltageSource( + voltage_out=self.gnd.link().voltage + actual_voltage, + current_limits=(0, 2) * Amp, # arbitrary assuming low capacity, 1 C discharge + ) + ) + self.gnd.init_from(Ground()) + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.actual_capacity, (2, 3.6) * Amp) # TODO should be A*h + + self.footprint( + "U", + "Battery:BatteryHolder_Keystone_1042_1x18650", + { + "1": self.pwr, + "2": self.gnd, + }, + mfr="Keystone", + part="1042", + ) class AaBattery(Battery, FootprintBlock): - """AA battery holder supporting alkaline and rechargeable chemistries.""" - def __init__(self, voltage: RangeLike = (1.0, 1.6)*Volt, *args: Any, - actual_voltage: RangeLike = (1.0, 1.6)*Volt, **kwargs: Any) -> None: - super().__init__(voltage, *args, **kwargs) - self.gnd.init_from(Ground()) - self.pwr.init_from(VoltageSource( - voltage_out=self.gnd.link().voltage + actual_voltage, - current_limits=(0, 1)*Amp, - )) - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.actual_capacity, (2, 3)*Amp) # TODO should be A*h - - self.footprint( - 'U', 'Battery:BatteryHolder_Keystone_2460_1xAA', - { - '1': self.pwr, - '2': self.gnd, - }, - mfr='Keystone', part='2460' - ) + """AA battery holder supporting alkaline and rechargeable chemistries.""" + + def __init__( + self, + voltage: RangeLike = (1.0, 1.6) * Volt, + *args: Any, + actual_voltage: RangeLike = (1.0, 1.6) * Volt, + **kwargs: Any, + ) -> None: + super().__init__(voltage, *args, **kwargs) + self.gnd.init_from(Ground()) + self.pwr.init_from( + VoltageSource( + voltage_out=self.gnd.link().voltage + actual_voltage, + current_limits=(0, 1) * Amp, + ) + ) + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.actual_capacity, (2, 3) * Amp) # TODO should be A*h + + self.footprint( + "U", + "Battery:BatteryHolder_Keystone_2460_1xAA", + { + "1": self.pwr, + "2": self.gnd, + }, + mfr="Keystone", + part="2460", + ) class AaBatteryStack(Battery, GeneratorBlock): """AA Alkaline battery stack that generates batteries in series""" - def __init__(self, count: IntLike = 1, *, cell_actual_voltage: RangeLike = (1.0, 1.6)*Volt): + + def __init__(self, count: IntLike = 1, *, cell_actual_voltage: RangeLike = (1.0, 1.6) * Volt): super().__init__(voltage=Range.all()) # no voltage spec passed in self.count = self.ArgParameter(count) self.cell_actual_voltage = self.ArgParameter(cell_actual_voltage) @@ -96,18 +126,18 @@ def __init__(self, count: IntLike = 1, *, cell_actual_voltage: RangeLike = (1.0, def generate(self) -> None: super().generate() prev_cell: Optional[AaBattery] = None - prev_capacity_min: Union[FloatExpr, float] = float('inf') - prev_capacity_max: Union[FloatExpr, float] = float('inf') + prev_capacity_min: Union[FloatExpr, float] = float("inf") + prev_capacity_max: Union[FloatExpr, float] = float("inf") self.cell = ElementDict[AaBattery]() for i in range(self.get(self.count)): - self.cell[i] = cell = self.Block(AaBattery(actual_voltage=self.cell_actual_voltage)) - if prev_cell is None: # first cell, direct connect to gnd - self.connect(self.gnd, cell.gnd) - else: - self.connect(prev_cell.pwr.as_ground(self.pwr.link().current_drawn), cell.gnd) - prev_capacity_min = cell.actual_capacity.lower().min(prev_capacity_min) - prev_capacity_max = cell.actual_capacity.upper().min(prev_capacity_max) - prev_cell = cell + self.cell[i] = cell = self.Block(AaBattery(actual_voltage=self.cell_actual_voltage)) + if prev_cell is None: # first cell, direct connect to gnd + self.connect(self.gnd, cell.gnd) + else: + self.connect(prev_cell.pwr.as_ground(self.pwr.link().current_drawn), cell.gnd) + prev_capacity_min = cell.actual_capacity.lower().min(prev_capacity_min) + prev_capacity_max = cell.actual_capacity.upper().min(prev_capacity_max) + prev_cell = cell assert prev_cell is not None, "must generate >=1 cell" self.connect(self.pwr, prev_cell.pwr) diff --git a/edg/parts/BatteryCharger_Mcp73831.py b/edg/parts/BatteryCharger_Mcp73831.py index b14013326..8f97238c4 100644 --- a/edg/parts/BatteryCharger_Mcp73831.py +++ b/edg/parts/BatteryCharger_Mcp73831.py @@ -5,77 +5,87 @@ class Mcp73831_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self, actual_charging_current: RangeLike) -> None: - super().__init__() - - self.actual_charging_current = self.ArgParameter(actual_charging_current) - - self.vss = self.Port(Ground()) - self.vdd = self.Port(VoltageSink( - voltage_limits=(3.75, 6)*Volt, # Section 1, DC characteristics - current_draw=(0.0001, 1.5)*mAmp + (0, self.actual_charging_current.upper()) - )) - - self.stat = self.Port(DigitalSource.from_supply( - self.vss, self.vdd, - current_limits=(-25, 35)*mAmp - ), optional=True) - self.vbat = self.Port(VoltageSource( - voltage_out=(4.168, 4.232)*Volt, # -2 variant - current_limits=self.actual_charging_current.hull(0 * Amp(tol=0)) - )) - self.prog = self.Port(Passive()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.stat, - '2': self.vss, - '3': self.vbat, - '4': self.vdd, - '5': self.prog, - }, - mfr='Microchip Technology', part='MCP73831T-2ACI/OT', - datasheet='https://ww1.microchip.com/downloads/en/DeviceDoc/MCP73831-Family-Data-Sheet-DS20001984H.pdf' - ) - self.assign(self.lcsc_part, 'C424093') - self.assign(self.actual_basic_part, False) + def __init__(self, actual_charging_current: RangeLike) -> None: + super().__init__() + + self.actual_charging_current = self.ArgParameter(actual_charging_current) + + self.vss = self.Port(Ground()) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(3.75, 6) * Volt, # Section 1, DC characteristics + current_draw=(0.0001, 1.5) * mAmp + (0, self.actual_charging_current.upper()), + ) + ) + + self.stat = self.Port( + DigitalSource.from_supply(self.vss, self.vdd, current_limits=(-25, 35) * mAmp), optional=True + ) + self.vbat = self.Port( + VoltageSource( + voltage_out=(4.168, 4.232) * Volt, # -2 variant + current_limits=self.actual_charging_current.hull(0 * Amp(tol=0)), + ) + ) + self.prog = self.Port(Passive()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.stat, + "2": self.vss, + "3": self.vbat, + "4": self.vdd, + "5": self.prog, + }, + mfr="Microchip Technology", + part="MCP73831T-2ACI/OT", + datasheet="https://ww1.microchip.com/downloads/en/DeviceDoc/MCP73831-Family-Data-Sheet-DS20001984H.pdf", + ) + self.assign(self.lcsc_part, "C424093") + self.assign(self.actual_basic_part, False) class Mcp73831(PowerConditioner, Block): - """Single-cell Li-ion / Li-poly charger, seemingly popular on Adafruit and Sparkfun boards.""" - def __init__(self, charging_current: RangeLike) -> None: - super().__init__() - - self.ic = self.Block(Mcp73831_Device(actual_charging_current=RangeExpr())) - - self.pwr_bat = self.Export(self.ic.vbat, [Output]) - self.pwr = self.Export(self.ic.vdd, [Input, Power]) - self.gnd = self.Export(self.ic.vss, [Common]) - self.stat = self.Export(self.ic.stat, optional=True) # hi-Z when not charging, low when charging, high when done - - self.charging_current = self.ArgParameter(charging_current) - - @override - def contents(self) -> None: - super().contents() - - self.vdd_cap = self.Block( # not formally specified on the datasheet, but this is used in the reference board - DecouplingCapacitor(4.7*uFarad(tol=0.2)) - ).connected(self.gnd, self.pwr) - - self.vbat_cap = self.Block( # can be ceramic, tantalum, aluminum electrolytic - DecouplingCapacitor(4.7*uFarad(tol=0.2)) - ).connected(self.gnd, self.pwr_bat) - - self.prog_res = self.Block(Resistor( - resistance=(1 / self.charging_current).shrink_multiply(Range.from_tolerance(1000, 0.1)) - )) - self.connect(self.prog_res.a, self.ic.prog) - self.connect(self.prog_res.b.adapt_to(Ground()), self.gnd) - # tolerance is a guess - self.assign(self.ic.actual_charging_current, 1000*Volt(tol=0.1) / self.prog_res.actual_resistance) - self.require(self.prog_res.actual_resistance.within((2, 67)*kOhm), "prog must be within charge impedance range") + """Single-cell Li-ion / Li-poly charger, seemingly popular on Adafruit and Sparkfun boards.""" + + def __init__(self, charging_current: RangeLike) -> None: + super().__init__() + + self.ic = self.Block(Mcp73831_Device(actual_charging_current=RangeExpr())) + + self.pwr_bat = self.Export(self.ic.vbat, [Output]) + self.pwr = self.Export(self.ic.vdd, [Input, Power]) + self.gnd = self.Export(self.ic.vss, [Common]) + self.stat = self.Export( + self.ic.stat, optional=True + ) # hi-Z when not charging, low when charging, high when done + + self.charging_current = self.ArgParameter(charging_current) + + @override + def contents(self) -> None: + super().contents() + + self.vdd_cap = self.Block( # not formally specified on the datasheet, but this is used in the reference board + DecouplingCapacitor(4.7 * uFarad(tol=0.2)) + ).connected(self.gnd, self.pwr) + + self.vbat_cap = self.Block( # can be ceramic, tantalum, aluminum electrolytic + DecouplingCapacitor(4.7 * uFarad(tol=0.2)) + ).connected(self.gnd, self.pwr_bat) + + self.prog_res = self.Block( + Resistor(resistance=(1 / self.charging_current).shrink_multiply(Range.from_tolerance(1000, 0.1))) + ) + self.connect(self.prog_res.a, self.ic.prog) + self.connect(self.prog_res.b.adapt_to(Ground()), self.gnd) + # tolerance is a guess + self.assign(self.ic.actual_charging_current, 1000 * Volt(tol=0.1) / self.prog_res.actual_resistance) + self.require( + self.prog_res.actual_resistance.within((2, 67) * kOhm), "prog must be within charge impedance range" + ) diff --git a/edg/parts/BatteryProtector_S8261A.py b/edg/parts/BatteryProtector_S8261A.py index 13b7c3a02..b020b2c10 100644 --- a/edg/parts/BatteryProtector_S8261A.py +++ b/edg/parts/BatteryProtector_S8261A.py @@ -5,91 +5,93 @@ class S8261A_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.vss = self.Port(Ground()) - self.vdd = self.Port(VoltageSink.from_gnd( - self.vss, - voltage_limits=(-0.3, 12) * Volt, - current_draw=(0.7, 5.5) * uAmp - )) - - self.vm = self.Port(Passive()) - self.do = self.Port(Passive()) - self.co = self.Port(Passive()) - - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', - { - '1': self.do, - '2': self.vm, - '3': self.co, - # '4': no connection pin, - '5': self.vdd, - '6': self.vss, - }, - mfr='ABLIC', part='S-8261ABJMD-G3JT2S', - datasheet='https://www.mouser.com/datasheet/2/360/S8200A_E-1365901.pdf' - ) - self.assign(self.lcsc_part, 'C28081') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + + self.vss = self.Port(Ground()) + self.vdd = self.Port( + VoltageSink.from_gnd(self.vss, voltage_limits=(-0.3, 12) * Volt, current_draw=(0.7, 5.5) * uAmp) + ) + + self.vm = self.Port(Passive()) + self.do = self.Port(Passive()) + self.co = self.Port(Passive()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-6", + { + "1": self.do, + "2": self.vm, + "3": self.co, + # '4': no connection pin, + "5": self.vdd, + "6": self.vss, + }, + mfr="ABLIC", + part="S-8261ABJMD-G3JT2S", + datasheet="https://www.mouser.com/datasheet/2/360/S8200A_E-1365901.pdf", + ) + self.assign(self.lcsc_part, "C28081") + self.assign(self.actual_basic_part, False) + class S8261A(PowerConditioner, Block): - """1-cell LiIon/LiPo Battery protection IC protecting against overcharge, overdischarge, over current. - """ - def __init__(self) -> None: - super().__init__() - - self.ic = self.Block(S8261A_Device()) - - self.gnd_in = self.Export(self.ic.vss) - self.pwr_in = self.Port(VoltageSink.empty()) - self.gnd_out = self.Port(Ground.empty()) - self.pwr_out = self.Port(VoltageSource.empty()) - - self.do_fet = self.Block(Fet.NFet( - drain_current=self.pwr_in.link().current_drawn, - gate_voltage=self.pwr_in.link().voltage, - rds_on=(0, 0.1)*Ohm, - drain_voltage=self.pwr_in.link().voltage - )) - self.co_fet = self.Block(Fet.NFet( - drain_current=self.pwr_in.link().current_drawn, - gate_voltage=self.pwr_in.link().voltage, - rds_on=(0, 0.1)*Ohm, - drain_voltage=self.pwr_in.link().voltage - )) - - @override - def contents(self) -> None: - super().contents() - - self.vdd_res = self.Block( - SeriesPowerResistor(470 * Ohm(tol=0.10)) # while 470 is preferred, the actual acceptable range is 300-1k - ).connected(self.pwr_in, self.ic.vdd) - - self.connect(self.pwr_in, self.pwr_out) - - self.vdd_vss_cap = self.Block( - DecouplingCapacitor(0.1 * uFarad(tol=0.10)) - ).connected(self.ic.vss, self.ic.vdd) - - # do fet - self.connect(self.ic.vss, self.do_fet.source.adapt_to(Ground())) - self.connect(self.ic.do, self.do_fet.gate) - - # do co - self.connect(self.do_fet.drain, self.co_fet.drain) - - # co fet - self.connect(self.gnd_out, self.co_fet.source.adapt_to(Ground())) - self.connect(self.ic.co, self.co_fet.gate) - - self.vm_res = self.Block(Resistor(2 * kOhm(tol=0.10))) - self.connect(self.vm_res.a.adapt_to(Ground()), self.gnd_out) - self.connect(self.vm_res.b, self.ic.vm) + """1-cell LiIon/LiPo Battery protection IC protecting against overcharge, overdischarge, over current.""" + + def __init__(self) -> None: + super().__init__() + + self.ic = self.Block(S8261A_Device()) + + self.gnd_in = self.Export(self.ic.vss) + self.pwr_in = self.Port(VoltageSink.empty()) + self.gnd_out = self.Port(Ground.empty()) + self.pwr_out = self.Port(VoltageSource.empty()) + + self.do_fet = self.Block( + Fet.NFet( + drain_current=self.pwr_in.link().current_drawn, + gate_voltage=self.pwr_in.link().voltage, + rds_on=(0, 0.1) * Ohm, + drain_voltage=self.pwr_in.link().voltage, + ) + ) + self.co_fet = self.Block( + Fet.NFet( + drain_current=self.pwr_in.link().current_drawn, + gate_voltage=self.pwr_in.link().voltage, + rds_on=(0, 0.1) * Ohm, + drain_voltage=self.pwr_in.link().voltage, + ) + ) + + @override + def contents(self) -> None: + super().contents() + + self.vdd_res = self.Block( + SeriesPowerResistor(470 * Ohm(tol=0.10)) # while 470 is preferred, the actual acceptable range is 300-1k + ).connected(self.pwr_in, self.ic.vdd) + + self.connect(self.pwr_in, self.pwr_out) + + self.vdd_vss_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.10))).connected(self.ic.vss, self.ic.vdd) + + # do fet + self.connect(self.ic.vss, self.do_fet.source.adapt_to(Ground())) + self.connect(self.ic.do, self.do_fet.gate) + + # do co + self.connect(self.do_fet.drain, self.co_fet.drain) + + # co fet + self.connect(self.gnd_out, self.co_fet.source.adapt_to(Ground())) + self.connect(self.ic.co, self.co_fet.gate) + + self.vm_res = self.Block(Resistor(2 * kOhm(tol=0.10))) + self.connect(self.vm_res.a.adapt_to(Ground()), self.gnd_out) + self.connect(self.vm_res.b, self.ic.vm) diff --git a/edg/parts/Bldc_Drv8313.py b/edg/parts/Bldc_Drv8313.py index 37522ed05..219507c58 100644 --- a/edg/parts/Bldc_Drv8313.py +++ b/edg/parts/Bldc_Drv8313.py @@ -10,14 +10,18 @@ class Drv8313_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() - self.vm = self.Port(VoltageSink( # one 0.1uF capacitor per supply pin and a bulk Vm capacitor - voltage_limits=(8, 60)*Volt, # Table 6.3 Vm - current_draw=RangeExpr(), - )) - self.v3p3 = self.Port(VoltageSource( # internal regulator, bypass with 6.3v, 0.47uF capacitor - voltage_out=(3.1, 3.52)*Volt, # Table 6.5 V3P3 voltage - current_limits=(0, 10)*mAmp, # Table 6.3 max V3P3 load current - )) + self.vm = self.Port( + VoltageSink( # one 0.1uF capacitor per supply pin and a bulk Vm capacitor + voltage_limits=(8, 60) * Volt, # Table 6.3 Vm + current_draw=RangeExpr(), + ) + ) + self.v3p3 = self.Port( + VoltageSource( # internal regulator, bypass with 6.3v, 0.47uF capacitor + voltage_out=(3.1, 3.52) * Volt, # Table 6.5 V3P3 voltage + current_limits=(0, 10) * mAmp, # Table 6.3 max V3P3 load current + ) + ) self.vcp = self.Port(Passive()) # charge pump, 16V 0.1uF capacitor to Vm self.gnd = self.Port(Ground()) @@ -26,8 +30,7 @@ def __init__(self) -> None: self.outs = self.Port(Vector(DigitalSource.empty())) self.din_model = DigitalSink( # nSleep, ENx, INx - internally pulled down 100k (Table 6.5) - voltage_limits=(-0.3, 5.25)*Volt, # to input high voltage max - input_thresholds=(0.7, 2.2) + voltage_limits=(-0.3, 5.25) * Volt, input_thresholds=(0.7, 2.2) # to input high voltage max ) self.nreset = self.Port(self.din_model) # required to be driven, to clear fault conditions self.nsleep = self.Port(self.din_model) # required, though can be tied high @@ -41,71 +44,70 @@ def __init__(self) -> None: @override def contents(self) -> None: out_model = DigitalSource.from_supply( - self.gnd, self.vm, - current_limits=(-2.5, 2.5)*Amp # peak current, section 1 + self.gnd, self.vm, current_limits=(-2.5, 2.5) * Amp # peak current, section 1 ) channel_currents = [] out_connected = [] - for i in ['1', '2', '3']: + for i in ["1", "2", "3"]: en_i = self.ens.append_elt(self.din_model, i) in_i = self.ins.append_elt(self.din_model, i) out_i = self.outs.append_elt(out_model, i) self.pgnds.append_elt(Passive(), i) self.require(out_i.is_connected().implies(en_i.is_connected() & in_i.is_connected())) - channel_currents.append( - out_i.is_connected().then_else(out_i.link().current_drawn.abs().upper(), 0*mAmp) - ) + channel_currents.append(out_i.is_connected().then_else(out_i.link().current_drawn.abs().upper(), 0 * mAmp)) out_connected.append(out_i.is_connected()) overall_current = functools.reduce(lambda a, b: a.max(b), channel_currents) - self.assign(self.vm.current_draw, (0.5, 5)*mAmp + # Table 6.5 Vm sleep typ to operating max - (0, overall_current)) + self.assign( + self.vm.current_draw, (0.5, 5) * mAmp + (0, overall_current) # Table 6.5 Vm sleep typ to operating max + ) any_out_connected = functools.reduce(lambda a, b: a | b, out_connected) self.require(any_out_connected) self.footprint( - 'U', 'Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.85x5.4mm_ThermalVias', + "U", + "Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.85x5.4mm_ThermalVias", { - '1': self.cpl, - '2': self.cph, - '3': self.vcp, - '4': self.vm, - '5': self.outs['1'], - '6': self.pgnds['1'], - '7': self.pgnds['2'], - '8': self.outs['2'], - '9': self.outs['3'], - '10': self.pgnds['3'], - '11': self.vm, - '12': self.gnd, # compp, can be grounded if unused (datasheet 10.1) - '13': self.gnd, # compn, can be grounded if unused (datasheet 10.1) - '14': self.gnd, - '15': self.v3p3, - '16': self.nreset, - '17': self.nsleep, - '18': self.nfault, # open-drain fault status (requires external pullup) - '19': self.gnd, # ncompo, can be grounded if unused (datasheet 10.1) - '20': self.gnd, - '21': self.gnd, # NC, grounded when unused in example layouts - '22': self.ens['3'], - '23': self.ins['3'], - '24': self.ens['2'], - '25': self.ins['2'], - '26': self.ens['1'], - '27': self.ins['1'], - '28': self.gnd, - - '29': self.gnd, # exposed pad + "1": self.cpl, + "2": self.cph, + "3": self.vcp, + "4": self.vm, + "5": self.outs["1"], + "6": self.pgnds["1"], + "7": self.pgnds["2"], + "8": self.outs["2"], + "9": self.outs["3"], + "10": self.pgnds["3"], + "11": self.vm, + "12": self.gnd, # compp, can be grounded if unused (datasheet 10.1) + "13": self.gnd, # compn, can be grounded if unused (datasheet 10.1) + "14": self.gnd, + "15": self.v3p3, + "16": self.nreset, + "17": self.nsleep, + "18": self.nfault, # open-drain fault status (requires external pullup) + "19": self.gnd, # ncompo, can be grounded if unused (datasheet 10.1) + "20": self.gnd, + "21": self.gnd, # NC, grounded when unused in example layouts + "22": self.ens["3"], + "23": self.ins["3"], + "24": self.ens["2"], + "25": self.ins["2"], + "26": self.ens["1"], + "27": self.ins["1"], + "28": self.gnd, + "29": self.gnd, # exposed pad }, - mfr='Texas Instruments', part='DRV8313PWP', - datasheet='https://www.ti.com/lit/ds/symlink/drv8313.pdf' + mfr="Texas Instruments", + part="DRV8313PWP", + datasheet="https://www.ti.com/lit/ds/symlink/drv8313.pdf", ) - self.assign(self.lcsc_part, 'C92482') + self.assign(self.lcsc_part, "C92482") class Drv8313(BldcDriver, GeneratorBlock): - def __init__(self, *, risense_res: RangeLike = 100*mOhm(tol=0.05)) -> None: + def __init__(self, *, risense_res: RangeLike = 100 * mOhm(tol=0.05)) -> None: super().__init__() self.ic = self.Block(Drv8313_Device()) self.pwr = self.Export(self.ic.vm) @@ -125,24 +127,25 @@ def __init__(self, *, risense_res: RangeLike = 100*mOhm(tol=0.05)) -> None: @override def contents(self) -> None: super().contents() - self.vm_cap_bulk = self.Block(DecouplingCapacitor((10*0.8, 100)*uFarad)).connected(self.gnd, self.ic.vm) - self.vm_cap1 = self.Block(DecouplingCapacitor((0.1*0.8, 100)*uFarad)).connected(self.gnd, self.ic.vm) - self.vm_cap2 = self.Block(DecouplingCapacitor((0.1*0.8, 100)*uFarad)).connected(self.gnd, self.ic.vm) + self.vm_cap_bulk = self.Block(DecouplingCapacitor((10 * 0.8, 100) * uFarad)).connected(self.gnd, self.ic.vm) + self.vm_cap1 = self.Block(DecouplingCapacitor((0.1 * 0.8, 100) * uFarad)).connected(self.gnd, self.ic.vm) + self.vm_cap2 = self.Block(DecouplingCapacitor((0.1 * 0.8, 100) * uFarad)).connected(self.gnd, self.ic.vm) # TODO datasheet recommends 6.3v-rated cap, here we just derive it from the voltage rail - self.v3p3_cap = self.Block(DecouplingCapacitor(0.47*uFarad(tol=0.2))).connected(self.gnd, self.ic.v3p3) + self.v3p3_cap = self.Block(DecouplingCapacitor(0.47 * uFarad(tol=0.2))).connected(self.gnd, self.ic.v3p3) vm_voltage = self.pwr.link().voltage - self.gnd.link().voltage - self.cp_cap = self.Block(Capacitor(0.01*uFarad(tol=0.2), vm_voltage)) + self.cp_cap = self.Block(Capacitor(0.01 * uFarad(tol=0.2), vm_voltage)) self.connect(self.cp_cap.pos, self.ic.cph) self.connect(self.cp_cap.neg, self.ic.cpl) - self.vcp_cap = self.Block(Capacitor(0.1*uFarad(tol=0.2), (0, 16)*Volt)) + self.vcp_cap = self.Block(Capacitor(0.1 * uFarad(tol=0.2), (0, 16) * Volt)) self.connect(self.vcp_cap.pos, self.ic.vcp) self.connect(self.vcp_cap.neg.adapt_to(VoltageSink()), self.ic.vm) - self.nsleep_default = self.Block(DigitalSourceConnected()) \ - .out_with_default(self.ic.nsleep, self.nsleep, self.ic.v3p3.as_digital_source()) + self.nsleep_default = self.Block(DigitalSourceConnected()).out_with_default( + self.ic.nsleep, self.nsleep, self.ic.v3p3.as_digital_source() + ) @override def generate(self) -> None: @@ -152,16 +155,16 @@ def generate(self) -> None: self.pgnd_res = ElementDict[CurrentSenseResistor]() self.pgnd_sense.defined() self.outs.defined() - for i in ['1', '2', '3']: + for i in ["1", "2", "3"]: pgnd_pin = self.ic.pgnds.request(i) out = self.outs.append_elt(DigitalSource.empty(), i) self.connect(out, self.ic.outs.request(i)) if i in pgnd_requested: if gnd_voltage_source is None: gnd_voltage_source = self.gnd.as_voltage_source() - res = self.pgnd_res[i] = self.Block(CurrentSenseResistor( - resistance=self.risense_res, sense_in_reqd=False - )).connected(gnd_voltage_source, pgnd_pin.adapt_to(VoltageSink(current_draw=out.link().current_drawn))) + res = self.pgnd_res[i] = self.Block( + CurrentSenseResistor(resistance=self.risense_res, sense_in_reqd=False) + ).connected(gnd_voltage_source, pgnd_pin.adapt_to(VoltageSink(current_draw=out.link().current_drawn))) self.connect(self.pgnd_sense.append_elt(AnalogSource.empty(), i), res.sense_out) else: self.connect(pgnd_pin.adapt_to(Ground()), self.gnd) diff --git a/edg/parts/BoostConverter_AnalogDevices.py b/edg/parts/BoostConverter_AnalogDevices.py index 1e4013d43..765263cb1 100644 --- a/edg/parts/BoostConverter_AnalogDevices.py +++ b/edg/parts/BoostConverter_AnalogDevices.py @@ -5,81 +5,92 @@ class Ltc3429_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - self.vin = self.Port(VoltageSink( - voltage_limits=(1.0, 4.4)*Volt, # maximum minimum startup voltage to abs. max Vin - # TODO quiescent current - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - self.sw = self.Port(VoltageSink()) - self.fb = self.Port(AnalogSink(impedance=(8000, float('inf')) * kOhm)) - self.vout = self.Port(VoltageSource( - voltage_out=output_voltage, - current_limits=self.sw.link().current_limits - )) - self.nshdn = self.Port(DigitalSink( - voltage_limits=(-0.3, 6) * Volt, - current_draw=(0.01, 1)*uAmp, - input_thresholds=(0.35, 1)*Volt - )) + def __init__(self, output_voltage: RangeLike): + super().__init__() + self.vin = self.Port( + VoltageSink( + voltage_limits=(1.0, 4.4) * Volt, # maximum minimum startup voltage to abs. max Vin + # TODO quiescent current + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + self.sw = self.Port(VoltageSink()) + self.fb = self.Port(AnalogSink(impedance=(8000, float("inf")) * kOhm)) + self.vout = self.Port(VoltageSource(voltage_out=output_voltage, current_limits=self.sw.link().current_limits)) + self.nshdn = self.Port( + DigitalSink( + voltage_limits=(-0.3, 6) * Volt, current_draw=(0.01, 1) * uAmp, input_thresholds=(0.35, 1) * Volt + ) + ) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', - { - '1': self.sw, - '2': self.gnd, - '3': self.fb, - '4': self.nshdn, - '5': self.vout, - '6': self.vin, - }, - mfr='Linear Technology', part='LTC3429BES6#TRMPBF', - datasheet='https://www.analog.com/media/en/technical-documentation/data-sheets/3429fa.pdf' - ) - self.assign(self.lcsc_part, 'C684773') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-6", + { + "1": self.sw, + "2": self.gnd, + "3": self.fb, + "4": self.nshdn, + "5": self.vout, + "6": self.vin, + }, + mfr="Linear Technology", + part="LTC3429BES6#TRMPBF", + datasheet="https://www.analog.com/media/en/technical-documentation/data-sheets/3429fa.pdf", + ) + self.assign(self.lcsc_part, "C684773") + self.assign(self.actual_basic_part, False) class Ltc3429(VoltageRegulatorEnableWrapper, DiscreteBoostConverter): - """Low-input-voltage boost converter (starts as low as 0.85V). - Pin-compatible with the less-expensive UM3429S""" - NMOS_CURRENT_LIMIT = 0.6 - @override - def contents(self) -> None: - super().contents() + """Low-input-voltage boost converter (starts as low as 0.85V). + Pin-compatible with the less-expensive UM3429S""" - self.assign(self.actual_frequency, (380, 630)*kHertz) - self.require(self.pwr_out.voltage_out.within((2.2, 4.3)*Volt)) # >4.3v requires external diode + NMOS_CURRENT_LIMIT = 0.6 - with self.implicit_connect( - ImplicitConnect(self.pwr_in, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.fb = imp.Block(FeedbackVoltageDivider( - output_voltage=(1.192, 1.268) * Volt, - impedance=(40, 400) * kOhm, # about 25 MOhm worst case input impedance, this is 100x below - assumed_input_voltage=self.output_voltage - )) - self.connect(self.fb.input, self.pwr_out) + @override + def contents(self) -> None: + super().contents() - self.ic = imp.Block(Ltc3429_Device(self.fb.actual_input_voltage)) - self.connect(self.ic.vout, self.pwr_out) - self.connect(self.fb.output, self.ic.fb) + self.assign(self.actual_frequency, (380, 630) * kHertz) + self.require(self.pwr_out.voltage_out.within((2.2, 4.3) * Volt)) # >4.3v requires external diode - # TODO add constraint on effective inductance and capacitance range - self.power_path = imp.Block(BoostConverterPowerPath( - self.pwr_in.link().voltage, self.fb.actual_input_voltage, self.actual_frequency, - self.pwr_out.link().current_drawn, (0, self.NMOS_CURRENT_LIMIT)*Amp, - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - )) - self.connect(self.power_path.pwr_out, self.pwr_out) - self.connect(self.power_path.switch, self.ic.sw) + with self.implicit_connect( + ImplicitConnect(self.pwr_in, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.fb = imp.Block( + FeedbackVoltageDivider( + output_voltage=(1.192, 1.268) * Volt, + impedance=(40, 400) * kOhm, # about 25 MOhm worst case input impedance, this is 100x below + assumed_input_voltage=self.output_voltage, + ) + ) + self.connect(self.fb.input, self.pwr_out) - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.nshdn + self.ic = imp.Block(Ltc3429_Device(self.fb.actual_input_voltage)) + self.connect(self.ic.vout, self.pwr_out) + self.connect(self.fb.output, self.ic.fb) + + # TODO add constraint on effective inductance and capacitance range + self.power_path = imp.Block( + BoostConverterPowerPath( + self.pwr_in.link().voltage, + self.fb.actual_input_voltage, + self.actual_frequency, + self.pwr_out.link().current_drawn, + (0, self.NMOS_CURRENT_LIMIT) * Amp, + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ) + ) + self.connect(self.power_path.pwr_out, self.pwr_out) + self.connect(self.power_path.switch, self.ic.sw) + + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.nshdn diff --git a/edg/parts/BoostConverter_DiodesInc.py b/edg/parts/BoostConverter_DiodesInc.py index fa1aa594f..0057e77bb 100644 --- a/edg/parts/BoostConverter_DiodesInc.py +++ b/edg/parts/BoostConverter_DiodesInc.py @@ -5,85 +5,102 @@ class Ap3012_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.pwr_in = self.Port(VoltageSink( - voltage_limits=(2.6, 16)*Volt, - # TODO quiescent current - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - self.sw = self.Port(VoltageSink()) - self.fb = self.Port(AnalogSink(impedance=(12500, float('inf')) * kOhm)) # based on input current spec + def __init__(self) -> None: + super().__init__() + self.pwr_in = self.Port( + VoltageSink( + voltage_limits=(2.6, 16) * Volt, + # TODO quiescent current + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + self.sw = self.Port(VoltageSink()) + self.fb = self.Port(AnalogSink(impedance=(12500, float("inf")) * kOhm)) # based on input current spec - self.nshdn = self.Port(DigitalSink( - voltage_limits=(0, 16) * Volt, - current_draw=(0, 55)*uAmp, - input_thresholds=(0.4, 1.5)*Volt - )) + self.nshdn = self.Port( + DigitalSink(voltage_limits=(0, 16) * Volt, current_draw=(0, 55) * uAmp, input_thresholds=(0.4, 1.5) * Volt) + ) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.sw, - '2': self.gnd, - '3': self.fb, - '4': self.nshdn, - '5': self.pwr_in, - }, - mfr='Diodes Incorporated', part='AP3012K', - datasheet='https://www.diodes.com/assets/Datasheets/AP3012.pdf' - ) - self.assign(self.lcsc_part, 'C460356') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.sw, + "2": self.gnd, + "3": self.fb, + "4": self.nshdn, + "5": self.pwr_in, + }, + mfr="Diodes Incorporated", + part="AP3012K", + datasheet="https://www.diodes.com/assets/Datasheets/AP3012.pdf", + ) + self.assign(self.lcsc_part, "C460356") + self.assign(self.actual_basic_part, False) class Ap3012(VoltageRegulatorEnableWrapper, DiscreteBoostConverter): - """Adjustable boost converter in SOT-23-5 with integrated switch""" - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.nshdn + """Adjustable boost converter in SOT-23-5 with integrated switch""" - @override - def contents(self) -> None: - super().contents() + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.nshdn - self.assign(self.actual_frequency, (1.1, 1.9)*MHertz) - self.require(self.pwr_out.voltage_out.within((1.33, 29)*Volt)) + @override + def contents(self) -> None: + super().contents() - with self.implicit_connect( - ImplicitConnect(self.pwr_in, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Ap3012_Device()) + self.assign(self.actual_frequency, (1.1, 1.9) * MHertz) + self.require(self.pwr_out.voltage_out.within((1.33, 29) * Volt)) - self.fb = imp.Block(FeedbackVoltageDivider( - output_voltage=(1.17, 1.33) * Volt, - impedance=(1, 10) * kOhm, - assumed_input_voltage=self.output_voltage - )) - self.connect(self.fb.input, self.pwr_out) - self.connect(self.fb.output, self.ic.fb) + with self.implicit_connect( + ImplicitConnect(self.pwr_in, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Ap3012_Device()) - self.power_path = imp.Block(BoostConverterPowerPath( - self.pwr_in.link().voltage, self.fb.actual_input_voltage, self.actual_frequency, - self.pwr_out.link().current_drawn, (0, 0.5)*Amp, - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - )) - self.connect(self.power_path.pwr_out, self.pwr_out) - self.connect(self.power_path.switch, self.ic.sw) + self.fb = imp.Block( + FeedbackVoltageDivider( + output_voltage=(1.17, 1.33) * Volt, + impedance=(1, 10) * kOhm, + assumed_input_voltage=self.output_voltage, + ) + ) + self.connect(self.fb.input, self.pwr_out) + self.connect(self.fb.output, self.ic.fb) - self.rect = self.Block(Diode( - reverse_voltage=(0, self.pwr_out.voltage_out.upper()), - current=self.pwr_out.link().current_drawn, - voltage_drop=(0, 0.4)*Volt, - reverse_recovery_time=(0, 500) * nSecond # guess from Digikey's classification for "fast recovery" - )) - self.connect(self.ic.sw, self.rect.anode.adapt_to(VoltageSink())) - self.connect(self.pwr_out, self.rect.cathode.adapt_to(VoltageSource( - voltage_out=self.fb.actual_input_voltage, - current_limits=self.power_path.switch.current_limits - ))) + self.power_path = imp.Block( + BoostConverterPowerPath( + self.pwr_in.link().voltage, + self.fb.actual_input_voltage, + self.actual_frequency, + self.pwr_out.link().current_drawn, + (0, 0.5) * Amp, + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ) + ) + self.connect(self.power_path.pwr_out, self.pwr_out) + self.connect(self.power_path.switch, self.ic.sw) + + self.rect = self.Block( + Diode( + reverse_voltage=(0, self.pwr_out.voltage_out.upper()), + current=self.pwr_out.link().current_drawn, + voltage_drop=(0, 0.4) * Volt, + reverse_recovery_time=(0, 500) * nSecond, # guess from Digikey's classification for "fast recovery" + ) + ) + self.connect(self.ic.sw, self.rect.anode.adapt_to(VoltageSink())) + self.connect( + self.pwr_out, + self.rect.cathode.adapt_to( + VoltageSource( + voltage_out=self.fb.actual_input_voltage, current_limits=self.power_path.switch.current_limits + ) + ), + ) diff --git a/edg/parts/BoostConverter_TexasInstruments.py b/edg/parts/BoostConverter_TexasInstruments.py index 85d797089..1be48d352 100644 --- a/edg/parts/BoostConverter_TexasInstruments.py +++ b/edg/parts/BoostConverter_TexasInstruments.py @@ -5,253 +5,288 @@ class Tps61040_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - vfb = Range(1.208, 1.258) - self.vfb = self.Parameter(RangeExpr(vfb*Volt)) - - self.sw = self.Port(VoltageSink()) # internal switch specs not defined - self.gnd = self.Port(Ground(), [Common]) - self.fb = self.Port(AnalogSink(impedance=(vfb.lower / (1*uAmp), float('inf')))) # based on bias current - self.vin = self.Port(VoltageSink( - voltage_limits=(1.8, 6)*Volt, - current_draw=(0.1, 50)*uAmp # quiescent current - ), [Power]) - self.en = self.Port(DigitalSink.from_supply( - self.gnd, self.vin, - voltage_limit_tolerance=(-0.3, 0.3), - input_threshold_abs=(0.4, 1.3)*Volt - )) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.sw, - '2': self.gnd, - '3': self.fb, - '4': self.en, - '5': self.vin, - }, - mfr='Texas Instruments', part='TPS61040DBVR', - datasheet='https://www.ti.com/lit/ds/symlink/tps61040.pdf' - ) - self.assign(self.lcsc_part, 'C7722') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + vfb = Range(1.208, 1.258) + self.vfb = self.Parameter(RangeExpr(vfb * Volt)) + + self.sw = self.Port(VoltageSink()) # internal switch specs not defined + self.gnd = self.Port(Ground(), [Common]) + self.fb = self.Port(AnalogSink(impedance=(vfb.lower / (1 * uAmp), float("inf")))) # based on bias current + self.vin = self.Port( + VoltageSink(voltage_limits=(1.8, 6) * Volt, current_draw=(0.1, 50) * uAmp), [Power] # quiescent current + ) + self.en = self.Port( + DigitalSink.from_supply( + self.gnd, self.vin, voltage_limit_tolerance=(-0.3, 0.3), input_threshold_abs=(0.4, 1.3) * Volt + ) + ) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.sw, + "2": self.gnd, + "3": self.fb, + "4": self.en, + "5": self.vin, + }, + mfr="Texas Instruments", + part="TPS61040DBVR", + datasheet="https://www.ti.com/lit/ds/symlink/tps61040.pdf", + ) + self.assign(self.lcsc_part, "C7722") + self.assign(self.actual_basic_part, False) class Tps61040(VoltageRegulatorEnableWrapper, DiscreteBoostConverter): - """PFM (DCM, discontinuous mode) boost converter in SOT-23-5""" - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.en - - @override - def contents(self) -> None: - super().contents() - - self.require(self.output_voltage >= self.pwr_in.link().voltage) # it's a boost converter - self.assign(self.actual_frequency, 1*MHertz(tol=0)) # up to 1 MHz, can be lower - - with self.implicit_connect( - ImplicitConnect(self.pwr_in, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Tps61040_Device()) - - self.fb = imp.Block(FeedbackVoltageDivider( - output_voltage=self.ic.vfb, - impedance=(10, 183) * kOhm, # datasheet recommends typ R2 <= 200k, max R1 2.2M - assumed_input_voltage=self.output_voltage - )) - self.connect(self.fb.input, self.pwr_out) - self.connect(self.fb.output, self.ic.fb) - - # TODO 10pF is the datasheet-suggested starting, point, but equation also available - self.cff = self.Block(Capacitor(10*pFarad(tol=0.2), voltage=self.pwr_in.link().voltage)) - self.connect(self.cff.pos.adapt_to(VoltageSink()), self.pwr_out) - self.connect(self.cff.neg.adapt_to(AnalogSink()), self.ic.fb) - - # power path calculation here - we don't use BoostConverterPowerPath since this IC operates in DCM - # and has different component sizing guidelines - vin = self.pwr_in.link().voltage - vout = self.pwr_out.link().voltage - iload = self.pwr_out.link().current_drawn - efficiency_est = Range(0.7, 0.85) # given by datasheet - - ilim = (350, 450)*mAmp # current limit determined by chip - off_prop_delay = 100*nSecond # internal turn-off propagation delay, typ - max_on_time = (4, 7.5)*uSecond - - # peak current is IC current limit + 100ns (typ) internal propagation delay - ipeak = ilim + vin * off_prop_delay - - # recommended inductance 2.2uH to 47uH, depending on application - # calculate inductance limits to allow achieving peak current within 6ns max - # inductor equation: v = L di/dt => imax = 1/L int(v dt) + i0 = > L = v * t / imax for constant v - ramp_l_max = vin.lower() * max_on_time.lower() / ilim.upper() - - # best achievable switching frequency, from inductor charging and discharging rates are: - # ton = L * ip / Vin, toff = L * ip / (Vout - Vin) - # fs = 1/(ton + toff) = 1 / ( L*ip/Vin + L*ip/(Vout-Vin) ) - # => 1 / ( L*ip*(Vout-Vin)/Vin*(Vout-Vin) + L*ip*Vin/((Vout-Vin)*Vin) ) - # => 1 / ( L*ip*(Vout-Vin+Vin)/Vin*(Vout-Vin) ) - # => Vin*(Vout-Vin) / (L*ip*Vout) - # and fs <= 1MHz - # note, above equation has maxima at Vin = Vout/2, Vout maximum - vout_fs_max = vout.upper() - vin_fs_max = (vin.lower() <= vout_fs_max / 2).then_else( - vout_fs_max / 2, - (vin.lower() > vout_fs_max / 2).then_else( - vin.lower(), vin.upper() - ) - ) - ramp_l_min = vin_fs_max * (vout_fs_max - vin_fs_max) / (1*MHertz * ipeak.lower() * vout_fs_max) - - self.inductor = self.Block(Inductor((ramp_l_min, ramp_l_max), ipeak, self.actual_frequency)) - self.connect(self.pwr_in, self.inductor.a.adapt_to(VoltageSink( - voltage_limits=RangeExpr.ALL, - current_draw=self.pwr_out.link().current_drawn * self.pwr_out.link().voltage / self.pwr_in.link().voltage - ))) - self.connect(self.ic.sw, self.inductor.b.adapt_to(VoltageSource())) # internal node, not modeled - - # max current delivered at this frequency is - # charge per cycle is ip / 2 * toff = L * ip^2 / (2 * (Vout - Vin)) - # current is charge per cycle * switching frequency - # => L * ip^2 / (2 * (Vout - Vin)) * Vin*(Vout-Vin) / (L*ip*Vout) - # => ip / 2 * Vin / Vout - # => ip * Vin / (2 * Vout) - max_current = efficiency_est * ipeak * vin / (2 * vout) - self.rect = self.Block(Diode( - reverse_voltage=(0, vout.upper()), - current=(0, ipeak.upper()), - voltage_drop=(0, 0.4)*Volt, # arbitrary - reverse_recovery_time=(0, 500) * nSecond # guess from Digikey's classification for "fast recovery" - )) - self.connect(self.ic.sw, self.rect.anode.adapt_to(VoltageSink())) - self.connect(self.pwr_out, self.rect.cathode.adapt_to(VoltageSource( - voltage_out=self.fb.actual_input_voltage, - current_limits=(0, max_current.upper()) - ))) - - self.in_cap = self.Block(DecouplingCapacitor( # recommended by the datasheet - capacitance=4.7*uFarad(tol=0.2) - )).connected(self.gnd, self.pwr_in) - - # capacitor i=C dv/dt => dv = i*dt / C = q / C - # using charge per cycle above: dv = L * ip^2 / (2 * (Vout - Vin)) / C - # C = 1/dv * L * ip^2 / (2 * (Vout - Vin)) - # TODO: is this simplified model correct? the datasheet accounts for current draw during the ton period, - # in addition to charge pumped during the toff cycle; but also qin must be balanced with qout - # TODO: could also reduce the capacitance by subtracting output draw during the t_off period - # C = 1/dv * [(L * ip^2 / (2 * (Vout - Vin)) - iout * toff] - # => 1/dv * [(L * ip^2 / (2 * (Vout - Vin)) - iout * L * ip / (Vout - Vin)] - # => (L * ip)/dv * [(ip / (2 * (Vout - Vin)) - iout / (Vout - Vin) - # => (L * ip)/(dv*(Vout - Vin) * [ip/2 - iout] - # note, this ripple is on top of the reference voltage hysteresis / tolerances, reflected in the - # output voltage tolerance - # this is substantially equivalent to the Output Capacitor Selection datasheet equation, minus capcitor ESR - output_capacitance = (self.inductor.actual_inductance.upper() * ipeak.upper() / ( - self.output_ripple_limit * (vout.lower() - vin.upper())) - * (ipeak.upper() / 2 - iload.lower()), float('inf')) - - self.out_cap = self.Block(DecouplingCapacitor( # recommended by the datasheet - capacitance=output_capacitance - )).connected(self.gnd, self.pwr_out) + """PFM (DCM, discontinuous mode) boost converter in SOT-23-5""" + + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.en + + @override + def contents(self) -> None: + super().contents() + + self.require(self.output_voltage >= self.pwr_in.link().voltage) # it's a boost converter + self.assign(self.actual_frequency, 1 * MHertz(tol=0)) # up to 1 MHz, can be lower + + with self.implicit_connect( + ImplicitConnect(self.pwr_in, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Tps61040_Device()) + + self.fb = imp.Block( + FeedbackVoltageDivider( + output_voltage=self.ic.vfb, + impedance=(10, 183) * kOhm, # datasheet recommends typ R2 <= 200k, max R1 2.2M + assumed_input_voltage=self.output_voltage, + ) + ) + self.connect(self.fb.input, self.pwr_out) + self.connect(self.fb.output, self.ic.fb) + + # TODO 10pF is the datasheet-suggested starting, point, but equation also available + self.cff = self.Block(Capacitor(10 * pFarad(tol=0.2), voltage=self.pwr_in.link().voltage)) + self.connect(self.cff.pos.adapt_to(VoltageSink()), self.pwr_out) + self.connect(self.cff.neg.adapt_to(AnalogSink()), self.ic.fb) + + # power path calculation here - we don't use BoostConverterPowerPath since this IC operates in DCM + # and has different component sizing guidelines + vin = self.pwr_in.link().voltage + vout = self.pwr_out.link().voltage + iload = self.pwr_out.link().current_drawn + efficiency_est = Range(0.7, 0.85) # given by datasheet + + ilim = (350, 450) * mAmp # current limit determined by chip + off_prop_delay = 100 * nSecond # internal turn-off propagation delay, typ + max_on_time = (4, 7.5) * uSecond + + # peak current is IC current limit + 100ns (typ) internal propagation delay + ipeak = ilim + vin * off_prop_delay + + # recommended inductance 2.2uH to 47uH, depending on application + # calculate inductance limits to allow achieving peak current within 6ns max + # inductor equation: v = L di/dt => imax = 1/L int(v dt) + i0 = > L = v * t / imax for constant v + ramp_l_max = vin.lower() * max_on_time.lower() / ilim.upper() + + # best achievable switching frequency, from inductor charging and discharging rates are: + # ton = L * ip / Vin, toff = L * ip / (Vout - Vin) + # fs = 1/(ton + toff) = 1 / ( L*ip/Vin + L*ip/(Vout-Vin) ) + # => 1 / ( L*ip*(Vout-Vin)/Vin*(Vout-Vin) + L*ip*Vin/((Vout-Vin)*Vin) ) + # => 1 / ( L*ip*(Vout-Vin+Vin)/Vin*(Vout-Vin) ) + # => Vin*(Vout-Vin) / (L*ip*Vout) + # and fs <= 1MHz + # note, above equation has maxima at Vin = Vout/2, Vout maximum + vout_fs_max = vout.upper() + vin_fs_max = (vin.lower() <= vout_fs_max / 2).then_else( + vout_fs_max / 2, (vin.lower() > vout_fs_max / 2).then_else(vin.lower(), vin.upper()) + ) + ramp_l_min = vin_fs_max * (vout_fs_max - vin_fs_max) / (1 * MHertz * ipeak.lower() * vout_fs_max) + + self.inductor = self.Block(Inductor((ramp_l_min, ramp_l_max), ipeak, self.actual_frequency)) + self.connect( + self.pwr_in, + self.inductor.a.adapt_to( + VoltageSink( + voltage_limits=RangeExpr.ALL, + current_draw=self.pwr_out.link().current_drawn + * self.pwr_out.link().voltage + / self.pwr_in.link().voltage, + ) + ), + ) + self.connect(self.ic.sw, self.inductor.b.adapt_to(VoltageSource())) # internal node, not modeled + + # max current delivered at this frequency is + # charge per cycle is ip / 2 * toff = L * ip^2 / (2 * (Vout - Vin)) + # current is charge per cycle * switching frequency + # => L * ip^2 / (2 * (Vout - Vin)) * Vin*(Vout-Vin) / (L*ip*Vout) + # => ip / 2 * Vin / Vout + # => ip * Vin / (2 * Vout) + max_current = efficiency_est * ipeak * vin / (2 * vout) + self.rect = self.Block( + Diode( + reverse_voltage=(0, vout.upper()), + current=(0, ipeak.upper()), + voltage_drop=(0, 0.4) * Volt, # arbitrary + reverse_recovery_time=(0, 500) * nSecond, # guess from Digikey's classification for "fast recovery" + ) + ) + self.connect(self.ic.sw, self.rect.anode.adapt_to(VoltageSink())) + self.connect( + self.pwr_out, + self.rect.cathode.adapt_to( + VoltageSource(voltage_out=self.fb.actual_input_voltage, current_limits=(0, max_current.upper())) + ), + ) + + self.in_cap = self.Block( + DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2)) # recommended by the datasheet + ).connected(self.gnd, self.pwr_in) + + # capacitor i=C dv/dt => dv = i*dt / C = q / C + # using charge per cycle above: dv = L * ip^2 / (2 * (Vout - Vin)) / C + # C = 1/dv * L * ip^2 / (2 * (Vout - Vin)) + # TODO: is this simplified model correct? the datasheet accounts for current draw during the ton period, + # in addition to charge pumped during the toff cycle; but also qin must be balanced with qout + # TODO: could also reduce the capacitance by subtracting output draw during the t_off period + # C = 1/dv * [(L * ip^2 / (2 * (Vout - Vin)) - iout * toff] + # => 1/dv * [(L * ip^2 / (2 * (Vout - Vin)) - iout * L * ip / (Vout - Vin)] + # => (L * ip)/dv * [(ip / (2 * (Vout - Vin)) - iout / (Vout - Vin) + # => (L * ip)/(dv*(Vout - Vin) * [ip/2 - iout] + # note, this ripple is on top of the reference voltage hysteresis / tolerances, reflected in the + # output voltage tolerance + # this is substantially equivalent to the Output Capacitor Selection datasheet equation, minus capcitor ESR + output_capacitance = ( + self.inductor.actual_inductance.upper() + * ipeak.upper() + / (self.output_ripple_limit * (vout.lower() - vin.upper())) + * (ipeak.upper() / 2 - iload.lower()), + float("inf"), + ) + + self.out_cap = self.Block( + DecouplingCapacitor(capacitance=output_capacitance) # recommended by the datasheet + ).connected(self.gnd, self.pwr_out) class Lm2733_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.pwr_in = self.Port(VoltageSink( - voltage_limits=(2.7, 14)*Volt, - current_draw=(0.000024, 3)*mAmp - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - self.sw = self.Port(VoltageSink()) - self.fb = self.Port(AnalogSink(impedance=(20500, float('inf')) * kOhm)) # based on FB bias current - - self.nshdn = self.Port(DigitalSink.from_supply( - self.gnd, self.pwr_in, - voltage_limit_tolerance=(-0.4, 0.3)*Volt, - input_threshold_abs=(0.5, 1.5)*Volt - )) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.sw, - '2': self.gnd, - '3': self.fb, - '4': self.nshdn, - '5': self.pwr_in, - }, - mfr='Texas Instruments', part='LM2733X', # X=1.6 MHz, Y=0.6 MHz - datasheet='https://www.ti.com/lit/ds/symlink/lm2733.pdf' - ) - self.assign(self.lcsc_part, 'C80169') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + self.pwr_in = self.Port( + VoltageSink(voltage_limits=(2.7, 14) * Volt, current_draw=(0.000024, 3) * mAmp), [Power] + ) + self.gnd = self.Port(Ground(), [Common]) + self.sw = self.Port(VoltageSink()) + self.fb = self.Port(AnalogSink(impedance=(20500, float("inf")) * kOhm)) # based on FB bias current + + self.nshdn = self.Port( + DigitalSink.from_supply( + self.gnd, self.pwr_in, voltage_limit_tolerance=(-0.4, 0.3) * Volt, input_threshold_abs=(0.5, 1.5) * Volt + ) + ) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.sw, + "2": self.gnd, + "3": self.fb, + "4": self.nshdn, + "5": self.pwr_in, + }, + mfr="Texas Instruments", + part="LM2733X", # X=1.6 MHz, Y=0.6 MHz + datasheet="https://www.ti.com/lit/ds/symlink/lm2733.pdf", + ) + self.assign(self.lcsc_part, "C80169") + self.assign(self.actual_basic_part, False) class Lm2733(VoltageRegulatorEnableWrapper, DiscreteBoostConverter): - """Adjustable boost converter in SOT-23-5 with integrated switch""" - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.nshdn + """Adjustable boost converter in SOT-23-5 with integrated switch""" + + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.nshdn + + @override + def contents(self) -> None: + import math - @override - def contents(self) -> None: - import math - super().contents() + super().contents() - self.assign(self.actual_frequency, (1.15, 1.85)*MHertz) + self.assign(self.actual_frequency, (1.15, 1.85) * MHertz) - with self.implicit_connect( + with self.implicit_connect( ImplicitConnect(self.pwr_in, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Lm2733_Device()) - - self.fb = imp.Block(FeedbackVoltageDivider( - output_voltage=(1.205, 1.255) * Volt, - impedance=(10, 100) * kOhm, - assumed_input_voltage=self.output_voltage - )) - self.connect(self.fb.input, self.pwr_out) - self.connect(self.fb.output, self.ic.fb) - - self.power_path = imp.Block(BoostConverterPowerPath( - self.pwr_in.link().voltage, self.fb.actual_input_voltage, self.actual_frequency, - self.pwr_out.link().current_drawn, (0, 1)*Amp, - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - )) - self.connect(self.power_path.pwr_out, self.pwr_out) - self.connect(self.power_path.switch, self.ic.sw) - - self.cf = self.Block(Capacitor( # arbitrary target tolerance for zero location for capacitance flexibility - capacitance=(1/(8000*Ohm(tol=0.35))).shrink_multiply(1/(2 * math.pi * self.fb.actual_rtop)), - voltage=self.pwr_out.voltage_out - )) - self.connect(self.cf.neg.adapt_to(AnalogSink()), self.ic.fb) - self.connect(self.cf.pos.adapt_to(VoltageSink()), self.pwr_out) - - self.rect = self.Block(Diode( - reverse_voltage=(0, self.pwr_out.voltage_out.upper()), - current=self.pwr_out.link().current_drawn, - voltage_drop=(0, 0.8)*Volt, - reverse_recovery_time=(0, 500) * nSecond # guess from Digikey's classification for "fast recovery" - )) - self.connect(self.ic.sw, self.rect.anode.adapt_to(VoltageSink())) - self.connect(self.pwr_out, self.rect.cathode.adapt_to(VoltageSource( - voltage_out=self.fb.actual_input_voltage, - current_limits=self.power_path.switch.current_limits - ))) - - self.require(self.pwr_out.voltage_out.upper() + self.rect.actual_voltage_drop.upper() <= 40, - "max SW voltage") + ) as imp: + self.ic = imp.Block(Lm2733_Device()) + + self.fb = imp.Block( + FeedbackVoltageDivider( + output_voltage=(1.205, 1.255) * Volt, + impedance=(10, 100) * kOhm, + assumed_input_voltage=self.output_voltage, + ) + ) + self.connect(self.fb.input, self.pwr_out) + self.connect(self.fb.output, self.ic.fb) + + self.power_path = imp.Block( + BoostConverterPowerPath( + self.pwr_in.link().voltage, + self.fb.actual_input_voltage, + self.actual_frequency, + self.pwr_out.link().current_drawn, + (0, 1) * Amp, + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ) + ) + self.connect(self.power_path.pwr_out, self.pwr_out) + self.connect(self.power_path.switch, self.ic.sw) + + self.cf = self.Block( + Capacitor( # arbitrary target tolerance for zero location for capacitance flexibility + capacitance=(1 / (8000 * Ohm(tol=0.35))).shrink_multiply(1 / (2 * math.pi * self.fb.actual_rtop)), + voltage=self.pwr_out.voltage_out, + ) + ) + self.connect(self.cf.neg.adapt_to(AnalogSink()), self.ic.fb) + self.connect(self.cf.pos.adapt_to(VoltageSink()), self.pwr_out) + + self.rect = self.Block( + Diode( + reverse_voltage=(0, self.pwr_out.voltage_out.upper()), + current=self.pwr_out.link().current_drawn, + voltage_drop=(0, 0.8) * Volt, + reverse_recovery_time=(0, 500) * nSecond, # guess from Digikey's classification for "fast recovery" + ) + ) + self.connect(self.ic.sw, self.rect.anode.adapt_to(VoltageSink())) + self.connect( + self.pwr_out, + self.rect.cathode.adapt_to( + VoltageSource( + voltage_out=self.fb.actual_input_voltage, current_limits=self.power_path.switch.current_limits + ) + ), + ) + + self.require( + self.pwr_out.voltage_out.upper() + self.rect.actual_voltage_drop.upper() <= 40, "max SW voltage" + ) diff --git a/edg/parts/BoostConverter_Torex.py b/edg/parts/BoostConverter_Torex.py index b997b03dd..9345fabb0 100644 --- a/edg/parts/BoostConverter_Torex.py +++ b/edg/parts/BoostConverter_Torex.py @@ -4,104 +4,121 @@ class Xc9142_Device(InternalSubcircuit, FootprintBlock, GeneratorBlock): - parts_output_voltage_current = [ # from Table 2, Vout and Ilim (min/typ, max) - ('18', Range(1.764, 1.836), Range(0.96, 2.30)), - ('25', Range(2.450, 2.550), Range(1.19, 2.30)), - ('30', Range(2.940, 3.060), Range(0.96, 2.30)), - ('33', Range(3.234, 3.366), Range(0.98, 2.30)), - ('50', Range(4.900, 5.100), Range(1.07, 2.30)), - ] - parts_frequency = [ # fosc - ('C', Range(1.02e6, 1.38e6)), # 1.2MHz - ('D', Range(2.40e6, 3.60e6)), # 3.0MHz - ] - parts_package = [ - ('MR-G', 'Package_TO_SOT_SMD:SOT-23-5'), - ] - - def __init__(self, output_voltage: RangeLike, frequency: RangeLike = Range.all()): - super().__init__() - - self.vin = self.Port(VoltageSink( - voltage_limits=(0.90, 6.0)*Volt, # maximum minimum startup voltage to max input voltage - # TODO quiescent current - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - self.ce = self.Port(DigitalSink.from_supply(self.gnd, self.vin, - voltage_limit_tolerance=(0, 5)*Volt, - input_threshold_abs=(0.2, 0.6)*Volt)) - self.sw = self.Port(VoltageSink()) - self.vout = self.Port(VoltageSource.empty()) - - self.output_voltage = self.ArgParameter(output_voltage) - self.frequency = self.ArgParameter(frequency) - self.generator_param(self.output_voltage, self.frequency) - - self.actual_current_limit = self.Parameter(RangeExpr()) # set by part number - self.actual_frequency = self.Parameter(RangeExpr()) # set by part number - - @override - def generate(self) -> None: - super().generate() - - part_number_voltage, part_voltage, part_current = [ - (part, part_voltage, part_current) for (part, part_voltage, part_current) in self.parts_output_voltage_current - if part_voltage in self.get(self.output_voltage)][0] - part_number_frequency, part_frequency = [ - (part, part_frequency) for (part, part_frequency) in self.parts_frequency - if part_frequency in self.get(self.frequency)][0] # lower frequency preferred, 'safer' option for compatibility - part_number_package, part_package = self.parts_package[0] # SOT-23-5 hardcoded for now - - self.assign(self.actual_frequency, part_frequency) - self.assign(self.actual_current_limit, (0, part_current.lower)) - self.vout.init_from(VoltageSource( - voltage_out=part_voltage, current_limits=self.sw.link().current_limits - )) - - self.footprint( - 'U', part_package, - { - '1': self.ce, - '2': self.gnd, - '3': self.vin, - '4': self.vout, - '5': self.sw, - }, - mfr='Torex Semiconductor Ltd', part=f'XC9142*{part_number_voltage}{part_number_frequency}{part_number_package}', - datasheet='https://www.torexsemi.com/file/xc9141/XC9141-XC9142.pdf' - ) + parts_output_voltage_current = [ # from Table 2, Vout and Ilim (min/typ, max) + ("18", Range(1.764, 1.836), Range(0.96, 2.30)), + ("25", Range(2.450, 2.550), Range(1.19, 2.30)), + ("30", Range(2.940, 3.060), Range(0.96, 2.30)), + ("33", Range(3.234, 3.366), Range(0.98, 2.30)), + ("50", Range(4.900, 5.100), Range(1.07, 2.30)), + ] + parts_frequency = [ # fosc + ("C", Range(1.02e6, 1.38e6)), # 1.2MHz + ("D", Range(2.40e6, 3.60e6)), # 3.0MHz + ] + parts_package = [ + ("MR-G", "Package_TO_SOT_SMD:SOT-23-5"), + ] + + def __init__(self, output_voltage: RangeLike, frequency: RangeLike = Range.all()): + super().__init__() + + self.vin = self.Port( + VoltageSink( + voltage_limits=(0.90, 6.0) * Volt, # maximum minimum startup voltage to max input voltage + # TODO quiescent current + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + self.ce = self.Port( + DigitalSink.from_supply( + self.gnd, self.vin, voltage_limit_tolerance=(0, 5) * Volt, input_threshold_abs=(0.2, 0.6) * Volt + ) + ) + self.sw = self.Port(VoltageSink()) + self.vout = self.Port(VoltageSource.empty()) + + self.output_voltage = self.ArgParameter(output_voltage) + self.frequency = self.ArgParameter(frequency) + self.generator_param(self.output_voltage, self.frequency) + + self.actual_current_limit = self.Parameter(RangeExpr()) # set by part number + self.actual_frequency = self.Parameter(RangeExpr()) # set by part number + + @override + def generate(self) -> None: + super().generate() + + part_number_voltage, part_voltage, part_current = [ + (part, part_voltage, part_current) + for (part, part_voltage, part_current) in self.parts_output_voltage_current + if part_voltage in self.get(self.output_voltage) + ][0] + part_number_frequency, part_frequency = [ + (part, part_frequency) + for (part, part_frequency) in self.parts_frequency + if part_frequency in self.get(self.frequency) + ][ + 0 + ] # lower frequency preferred, 'safer' option for compatibility + part_number_package, part_package = self.parts_package[0] # SOT-23-5 hardcoded for now + + self.assign(self.actual_frequency, part_frequency) + self.assign(self.actual_current_limit, (0, part_current.lower)) + self.vout.init_from(VoltageSource(voltage_out=part_voltage, current_limits=self.sw.link().current_limits)) + + self.footprint( + "U", + part_package, + { + "1": self.ce, + "2": self.gnd, + "3": self.vin, + "4": self.vout, + "5": self.sw, + }, + mfr="Torex Semiconductor Ltd", + part=f"XC9142*{part_number_voltage}{part_number_frequency}{part_number_package}", + datasheet="https://www.torexsemi.com/file/xc9141/XC9141-XC9142.pdf", + ) class Xc9142(Resettable, DiscreteBoostConverter, GeneratorBlock): - """Low-input-voltage boost converter (starts as low as 0.9V) with fixed output. - XC9142 has PWM/PFM functionality, compared to PWM only for XC9141. - Semi pin compatible with XC9140, LTC3525, MAX1724.""" - @override - def contents(self) -> None: - super().contents() - self.generator_param(self.reset.is_connected()) - - with self.implicit_connect( - ImplicitConnect(self.pwr_in, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Xc9142_Device(self.output_voltage)) - self.connect(self.ic.vout, self.pwr_out) - self.assign(self.actual_frequency, self.ic.actual_frequency) - - self.power_path = imp.Block(BoostConverterPowerPath( - self.pwr_in.link().voltage, self.ic.vout.voltage_out, self.actual_frequency, - self.pwr_out.link().current_drawn, self.ic.actual_current_limit, - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - )) - self.connect(self.power_path.pwr_out, self.pwr_out) - self.connect(self.power_path.switch, self.ic.sw) - - @override - def generate(self) -> None: - super().generate() - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.ce) - else: # CE resistor: recommended through a <1M resistor; must not be left open - self.ce_res = self.Block(PullupResistor(100*kOhm(tol=0.2))).connected(pwr=self.pwr_in, io=self.ic.ce) + """Low-input-voltage boost converter (starts as low as 0.9V) with fixed output. + XC9142 has PWM/PFM functionality, compared to PWM only for XC9141. + Semi pin compatible with XC9140, LTC3525, MAX1724.""" + + @override + def contents(self) -> None: + super().contents() + self.generator_param(self.reset.is_connected()) + + with self.implicit_connect( + ImplicitConnect(self.pwr_in, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Xc9142_Device(self.output_voltage)) + self.connect(self.ic.vout, self.pwr_out) + self.assign(self.actual_frequency, self.ic.actual_frequency) + + self.power_path = imp.Block( + BoostConverterPowerPath( + self.pwr_in.link().voltage, + self.ic.vout.voltage_out, + self.actual_frequency, + self.pwr_out.link().current_drawn, + self.ic.actual_current_limit, + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ) + ) + self.connect(self.power_path.pwr_out, self.pwr_out) + self.connect(self.power_path.switch, self.ic.sw) + + @override + def generate(self) -> None: + super().generate() + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.ce) + else: # CE resistor: recommended through a <1M resistor; must not be left open + self.ce_res = self.Block(PullupResistor(100 * kOhm(tol=0.2))).connected(pwr=self.pwr_in, io=self.ic.ce) diff --git a/edg/parts/BootstrapVoltageAdder.py b/edg/parts/BootstrapVoltageAdder.py index 392d7e549..5cef84e28 100644 --- a/edg/parts/BootstrapVoltageAdder.py +++ b/edg/parts/BootstrapVoltageAdder.py @@ -4,9 +4,9 @@ class BootstrapVoltageAdder(KiCadSchematicBlock, PowerConditioner, Block): - """Bipolar (positive and negative) voltage adder using a switched cap circuit. - """ - def __init__(self, frequency: RangeLike = 250*kHertz(tol=0), ripple_limit: FloatLike = 25*mVolt): + """Bipolar (positive and negative) voltage adder using a switched cap circuit.""" + + def __init__(self, frequency: RangeLike = 250 * kHertz(tol=0), ripple_limit: FloatLike = 25 * mVolt): super().__init__() self.gnd = self.Port(Ground.empty()) @@ -27,10 +27,13 @@ def contents(self) -> None: # TODO model diode forward voltage drops out_current = self.out_pos.link().current_drawn.hull(self.out_neg.link().current_drawn) - diode_model = Diode(reverse_voltage=self.pwm.link().voltage.hull(0*Volt(tol=0)), - current=out_current, voltage_drop=(0, 0.5)*Volt) - cap_fly_model = Capacitor(1*uFarad(tol=0.2), voltage=self.pwr_pos.link().voltage + self.pwm.link().voltage) - cap_bulk_model = Capacitor(1*uFarad(tol=0.2), voltage=self.pwm.link().voltage) + diode_model = Diode( + reverse_voltage=self.pwm.link().voltage.hull(0 * Volt(tol=0)), + current=out_current, + voltage_drop=(0, 0.5) * Volt, + ) + cap_fly_model = Capacitor(1 * uFarad(tol=0.2), voltage=self.pwr_pos.link().voltage + self.pwm.link().voltage) + cap_bulk_model = Capacitor(1 * uFarad(tol=0.2), voltage=self.pwm.link().voltage) self.d_pos_1 = self.Block(diode_model) self.d_pos_2 = self.Block(diode_model) @@ -50,28 +53,29 @@ def contents(self) -> None: # we assume (perhaps wrongly) that it will discharge to ripple_limit every cycle # and that the switching cap action will bring it back up by 1/2 ripple_limit, # for dV = ripple_limit/2 - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'gnd': Ground(), # protection only, no draw - 'pwr': VoltageSink(), - - 'pwr_pos': VoltageSink( - current_draw=out_current - ), - 'pwr_neg': VoltageSink( - current_draw=out_current - ), - 'pwm': DigitalSink( - current_draw=(-out_current.upper(), out_current.upper()) - ), - 'out_pos': VoltageSource( - self.pwr_pos.link().voltage + self.pwm.link().output_thresholds.upper(), - # NOTE - assumes flying cap = bulk cap - current_limits=(0, (self.c_pos.actual_capacitance * self.ripple_limit / 2 * self.frequency).lower()) - ), - 'out_neg': VoltageSource( - self.pwr_neg.link().voltage - self.pwm.link().output_thresholds.upper(), - # NOTE - assumes flying cap = bulk cap - current_limits=(0, (self.c_neg.actual_capacitance * self.ripple_limit / 2 * self.frequency).lower()) - ), - }) + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "gnd": Ground(), # protection only, no draw + "pwr": VoltageSink(), + "pwr_pos": VoltageSink(current_draw=out_current), + "pwr_neg": VoltageSink(current_draw=out_current), + "pwm": DigitalSink(current_draw=(-out_current.upper(), out_current.upper())), + "out_pos": VoltageSource( + self.pwr_pos.link().voltage + self.pwm.link().output_thresholds.upper(), + # NOTE - assumes flying cap = bulk cap + current_limits=( + 0, + (self.c_pos.actual_capacitance * self.ripple_limit / 2 * self.frequency).lower(), + ), + ), + "out_neg": VoltageSource( + self.pwr_neg.link().voltage - self.pwm.link().output_thresholds.upper(), + # NOTE - assumes flying cap = bulk cap + current_limits=( + 0, + (self.c_neg.actual_capacitance * self.ripple_limit / 2 * self.frequency).lower(), + ), + ), + }, + ) diff --git a/edg/parts/BuckBoostConverter_Custom.py b/edg/parts/BuckBoostConverter_Custom.py index 735777441..172f378dc 100644 --- a/edg/parts/BuckBoostConverter_Custom.py +++ b/edg/parts/BuckBoostConverter_Custom.py @@ -8,102 +8,112 @@ # These adapters are needed to properly orient the boost-side switch, since it outputs on the high side # and inputs in the center class VoltageSinkConnector(DummyDevice, NetBlock): - """Connects two voltage sinks together (FET top sink to exterior source).""" - def __init__(self, voltage_out: RangeLike, a_current_limits: RangeLike, b_current_limits: RangeLike) -> None: - super().__init__() - self.a = self.Port(VoltageSource( - voltage_out=voltage_out, - current_limits=a_current_limits - ), [Input]) # FET top: set output voltage, allow instantaneous current draw - self.b = self.Port(VoltageSource( - voltage_out=voltage_out, - current_limits=b_current_limits - ), [Output]) # exterior source: set output voltage + Ilim + """Connects two voltage sinks together (FET top sink to exterior source).""" + + def __init__(self, voltage_out: RangeLike, a_current_limits: RangeLike, b_current_limits: RangeLike) -> None: + super().__init__() + self.a = self.Port( + VoltageSource(voltage_out=voltage_out, current_limits=a_current_limits), [Input] + ) # FET top: set output voltage, allow instantaneous current draw + self.b = self.Port( + VoltageSource(voltage_out=voltage_out, current_limits=b_current_limits), [Output] + ) # exterior source: set output voltage + Ilim class VoltageSourceConnector(DummyDevice, NetBlock): - """Connects two voltage sources together (inductor output to FET center 'source').""" - def __init__(self, a_current_draw: RangeLike, b_current_draw: RangeLike) -> None: - super().__init__() - self.a = self.Port(VoltageSink( - current_draw=a_current_draw - ), [Input]) # FET top: set output voltage, allow instantaneous current draw - self.b = self.Port(VoltageSink( - current_draw=b_current_draw - ), [Output]) # exterior source: set output voltage + Ilim + """Connects two voltage sources together (inductor output to FET center 'source').""" + + def __init__(self, a_current_draw: RangeLike, b_current_draw: RangeLike) -> None: + super().__init__() + self.a = self.Port( + VoltageSink(current_draw=a_current_draw), [Input] + ) # FET top: set output voltage, allow instantaneous current draw + self.b = self.Port( + VoltageSink(current_draw=b_current_draw), [Output] + ) # exterior source: set output voltage + Ilim class CustomSyncBuckBoostConverterPwm(DiscreteBoostConverter, Resettable): - """Custom synchronous buck-boost with four PWMs for the switches. - Because of the MOSFET body diode, will probably be fine-ish if the buck low-side FET and the boost high-side FET - are not driven""" - def __init__(self, *args: Any, - frequency: RangeLike = (100, 1000)*kHertz, - ripple_ratio: RangeLike = (0.2, 0.5), - rds_on: RangeLike = (0, 1.0)*Ohm, - **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - self.pwr_logic = self.Port(VoltageSink.empty()) - self.buck_pwm = self.Port(DigitalSink.empty()) - self.boost_pwm = self.Port(DigitalSink.empty()) - - self.frequency = self.ArgParameter(frequency) - self.ripple_ratio = self.ArgParameter(ripple_ratio) - self.rds_on = self.ArgParameter(rds_on) - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.actual_frequency, self.frequency) - self.power_path = self.Block(BuckBoostConverterPowerPath( - self.pwr_in.link().voltage, self.output_voltage, self.actual_frequency, - self.pwr_out.link().current_drawn, Range.exact(0), - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - ripple_ratio=self.ripple_ratio, - )) - self.connect(self.power_path.pwr_in, self.pwr_in) - self.connect(self.power_path.pwr_out, self.pwr_out) - self.connect(self.power_path.gnd, self.gnd) - - self.buck_sw = self.Block(FetHalfBridge(frequency=self.frequency, fet_rds=self.rds_on)) - self.connect(self.buck_sw.gnd, self.gnd) - self.connect(self.buck_sw.pwr_logic, self.pwr_logic) - self.connect(self.buck_sw.with_mixin(HalfBridgePwm()).pwm_ctl, self.buck_pwm) - self.connect(self.buck_sw.with_mixin(Resettable()).reset, self.reset) - (self.pwr_in_force, ), _ = self.chain( # use average draw for power input draw - self.pwr_in, - self.Block(ForcedVoltageCurrentDraw(self.power_path.switch_in.current_draw)), - self.buck_sw.pwr - ) - self.l1 = self.connect(self.power_path.switch_in) # give the node a name - (self.sw_in_force, ), _ = self.chain( # current draw used to size FETs, size for peak current - self.buck_sw.out, - self.Block(ForcedVoltageCurrentDraw(self.power_path.actual_inductor_current_peak)), - self.power_path.switch_in - ) - - self.boost_sw = self.Block(FetHalfBridge(frequency=self.frequency, fet_rds=self.rds_on)) - self.connect(self.boost_sw.gnd, self.gnd) - self.connect(self.boost_sw.pwr_logic, self.pwr_logic) - self.connect(self.boost_sw.with_mixin(HalfBridgePwm()).pwm_ctl, self.boost_pwm) - self.connect(self.boost_sw.with_mixin(Resettable()).reset, self.reset) - sw_current_limits = self.buck_sw.actual_current_limits.intersect(self.boost_sw.actual_current_limits) - (self.pwr_out_force, ), _ = self.chain( # use average output limits for power out limits - self.boost_sw.pwr, - self.Block(VoltageSinkConnector(self.output_voltage, - Range.all(), # unused, port actually in reverse - self.power_path.switch_out.current_limits.intersect( - sw_current_limits - self.power_path.actual_inductor_current_ripple.upper() / 2 - ).intersect(Range.from_lower(0)))), - self.pwr_out - ) - self.l2 = self.connect(self.power_path.switch_out) # give the node a name - (self.sw_out_force, ), _ = self.chain( # current draw used to size FETs, size for peak current - self.power_path.switch_out, - self.Block(VoltageSourceConnector(Range.exact(0), - self.power_path.actual_inductor_current_peak)), - self.boost_sw.out - ) + """Custom synchronous buck-boost with four PWMs for the switches. + Because of the MOSFET body diode, will probably be fine-ish if the buck low-side FET and the boost high-side FET + are not driven""" + + def __init__( + self, + *args: Any, + frequency: RangeLike = (100, 1000) * kHertz, + ripple_ratio: RangeLike = (0.2, 0.5), + rds_on: RangeLike = (0, 1.0) * Ohm, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + + self.pwr_logic = self.Port(VoltageSink.empty()) + self.buck_pwm = self.Port(DigitalSink.empty()) + self.boost_pwm = self.Port(DigitalSink.empty()) + + self.frequency = self.ArgParameter(frequency) + self.ripple_ratio = self.ArgParameter(ripple_ratio) + self.rds_on = self.ArgParameter(rds_on) + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.actual_frequency, self.frequency) + self.power_path = self.Block( + BuckBoostConverterPowerPath( + self.pwr_in.link().voltage, + self.output_voltage, + self.actual_frequency, + self.pwr_out.link().current_drawn, + Range.exact(0), + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ripple_ratio=self.ripple_ratio, + ) + ) + self.connect(self.power_path.pwr_in, self.pwr_in) + self.connect(self.power_path.pwr_out, self.pwr_out) + self.connect(self.power_path.gnd, self.gnd) + + self.buck_sw = self.Block(FetHalfBridge(frequency=self.frequency, fet_rds=self.rds_on)) + self.connect(self.buck_sw.gnd, self.gnd) + self.connect(self.buck_sw.pwr_logic, self.pwr_logic) + self.connect(self.buck_sw.with_mixin(HalfBridgePwm()).pwm_ctl, self.buck_pwm) + self.connect(self.buck_sw.with_mixin(Resettable()).reset, self.reset) + (self.pwr_in_force,), _ = self.chain( # use average draw for power input draw + self.pwr_in, self.Block(ForcedVoltageCurrentDraw(self.power_path.switch_in.current_draw)), self.buck_sw.pwr + ) + self.l1 = self.connect(self.power_path.switch_in) # give the node a name + (self.sw_in_force,), _ = self.chain( # current draw used to size FETs, size for peak current + self.buck_sw.out, + self.Block(ForcedVoltageCurrentDraw(self.power_path.actual_inductor_current_peak)), + self.power_path.switch_in, + ) + + self.boost_sw = self.Block(FetHalfBridge(frequency=self.frequency, fet_rds=self.rds_on)) + self.connect(self.boost_sw.gnd, self.gnd) + self.connect(self.boost_sw.pwr_logic, self.pwr_logic) + self.connect(self.boost_sw.with_mixin(HalfBridgePwm()).pwm_ctl, self.boost_pwm) + self.connect(self.boost_sw.with_mixin(Resettable()).reset, self.reset) + sw_current_limits = self.buck_sw.actual_current_limits.intersect(self.boost_sw.actual_current_limits) + (self.pwr_out_force,), _ = self.chain( # use average output limits for power out limits + self.boost_sw.pwr, + self.Block( + VoltageSinkConnector( + self.output_voltage, + Range.all(), # unused, port actually in reverse + self.power_path.switch_out.current_limits.intersect( + sw_current_limits - self.power_path.actual_inductor_current_ripple.upper() / 2 + ).intersect(Range.from_lower(0)), + ) + ), + self.pwr_out, + ) + self.l2 = self.connect(self.power_path.switch_out) # give the node a name + (self.sw_out_force,), _ = self.chain( # current draw used to size FETs, size for peak current + self.power_path.switch_out, + self.Block(VoltageSourceConnector(Range.exact(0), self.power_path.actual_inductor_current_peak)), + self.boost_sw.out, + ) diff --git a/edg/parts/BuckConverter_Ap3418.py b/edg/parts/BuckConverter_Ap3418.py index 10bf6ca96..75c6c9574 100644 --- a/edg/parts/BuckConverter_Ap3418.py +++ b/edg/parts/BuckConverter_Ap3418.py @@ -5,77 +5,89 @@ class Ap3418_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.sw = self.Port(VoltageSource()) # internal switch specs not defined, only bulk current limit defined - self.pwr_in = self.Port(VoltageSink( - voltage_limits=(2.5, 5.5)*Volt, - current_draw=self.sw.link().current_drawn # TODO quiescent current - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - self.fb = self.Port(AnalogSink(impedance=(10, float('inf')) * MOhm)) # based on input current spec - self.en = self.Port(DigitalSink.from_supply( - self.gnd, self.pwr_in, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - input_threshold_abs=(0.4, 1.5)*Volt - )) + def __init__(self) -> None: + super().__init__() + self.sw = self.Port(VoltageSource()) # internal switch specs not defined, only bulk current limit defined + self.pwr_in = self.Port( + VoltageSink( + voltage_limits=(2.5, 5.5) * Volt, current_draw=self.sw.link().current_drawn # TODO quiescent current + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + self.fb = self.Port(AnalogSink(impedance=(10, float("inf")) * MOhm)) # based on input current spec + self.en = self.Port( + DigitalSink.from_supply( + self.gnd, self.pwr_in, voltage_limit_tolerance=(-0.3, 0.3) * Volt, input_threshold_abs=(0.4, 1.5) * Volt + ) + ) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.en, - '2': self.gnd, - '3': self.sw, - '4': self.pwr_in, - '5': self.fb, - }, - mfr='Diodes Incorporated', part='AP3418', - datasheet='https://www.diodes.com/assets/Datasheets/products_inactive_data/AP3418.pdf' - ) - self.assign(self.lcsc_part, 'C500769') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.en, + "2": self.gnd, + "3": self.sw, + "4": self.pwr_in, + "5": self.fb, + }, + mfr="Diodes Incorporated", + part="AP3418", + datasheet="https://www.diodes.com/assets/Datasheets/products_inactive_data/AP3418.pdf", + ) + self.assign(self.lcsc_part, "C500769") + self.assign(self.actual_basic_part, False) class Ap3418(VoltageRegulatorEnableWrapper, DiscreteBuckConverter): - """Adjustable synchronous buck converter in SOT-23-5 with integrated switch""" - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.en + """Adjustable synchronous buck converter in SOT-23-5 with integrated switch""" - @override - def contents(self) -> None: - super().contents() + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.en - self.assign(self.actual_frequency, (1.12, 1.68)*MHertz) + @override + def contents(self) -> None: + super().contents() - with self.implicit_connect( - ImplicitConnect(self.pwr_in, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Ap3418_Device()) + self.assign(self.actual_frequency, (1.12, 1.68) * MHertz) - self.fb = imp.Block(FeedbackVoltageDivider( - output_voltage=(0.588, 0.612) * Volt, - impedance=(10, 100) * kOhm, - assumed_input_voltage=self.output_voltage - )) - self.connect(self.fb.input, self.pwr_out) - self.connect(self.fb.output, self.ic.fb) + with self.implicit_connect( + ImplicitConnect(self.pwr_in, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Ap3418_Device()) - # TODO: the control mechanism requires a specific capacitor / inductor selection, datasheet 8.2.2.3 - self.power_path = imp.Block(BuckConverterPowerPath( - self.pwr_in.link().voltage, self.fb.actual_input_voltage, self.actual_frequency, - self.pwr_out.link().current_drawn, (0, 1.8)*Amp, - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - dutycycle_limit=(0, 1) - )) - # ForcedVoltage needed to provide a voltage value so current downstream can be calculated - # and then the power path can generate - (self.forced_out, ), _ = self.chain(self.power_path.pwr_out, - self.Block(ForcedVoltage(self.fb.actual_input_voltage)), - self.pwr_out) - self.connect(self.power_path.switch, self.ic.sw) + self.fb = imp.Block( + FeedbackVoltageDivider( + output_voltage=(0.588, 0.612) * Volt, + impedance=(10, 100) * kOhm, + assumed_input_voltage=self.output_voltage, + ) + ) + self.connect(self.fb.input, self.pwr_out) + self.connect(self.fb.output, self.ic.fb) + + # TODO: the control mechanism requires a specific capacitor / inductor selection, datasheet 8.2.2.3 + self.power_path = imp.Block( + BuckConverterPowerPath( + self.pwr_in.link().voltage, + self.fb.actual_input_voltage, + self.actual_frequency, + self.pwr_out.link().current_drawn, + (0, 1.8) * Amp, + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + dutycycle_limit=(0, 1), + ) + ) + # ForcedVoltage needed to provide a voltage value so current downstream can be calculated + # and then the power path can generate + (self.forced_out,), _ = self.chain( + self.power_path.pwr_out, self.Block(ForcedVoltage(self.fb.actual_input_voltage)), self.pwr_out + ) + self.connect(self.power_path.switch, self.ic.sw) diff --git a/edg/parts/BuckConverter_Custom.py b/edg/parts/BuckConverter_Custom.py index 2803d85ec..c767272fa 100644 --- a/edg/parts/BuckConverter_Custom.py +++ b/edg/parts/BuckConverter_Custom.py @@ -8,11 +8,16 @@ class CustomSyncBuckConverterIndependent(DiscreteBoostConverter): """Custom synchronous buck with two PWM inputs for the high and low side gate drivers. Because of the MOSFET body diode, will probably be fine-ish if the low side FET is not driven.""" - def __init__(self, *args: Any, - frequency: RangeLike = (100, 1000)*kHertz, - ripple_ratio: RangeLike = (0.2, 0.5), - voltage_drop: RangeLike = (0, 1)*Volt, rds_on: RangeLike = (0, 1.0)*Ohm, - **kwargs: Any) -> None: + + def __init__( + self, + *args: Any, + frequency: RangeLike = (100, 1000) * kHertz, + ripple_ratio: RangeLike = (0.2, 0.5), + voltage_drop: RangeLike = (0, 1) * Volt, + rds_on: RangeLike = (0, 1.0) * Ohm, + **kwargs: Any, + ) -> None: super().__init__(*args, **kwargs) self.pwr_logic = self.Port(VoltageSink.empty()) @@ -29,35 +34,43 @@ def contents(self) -> None: super().contents() self.assign(self.actual_frequency, self.frequency) - self.power_path = self.Block(BuckConverterPowerPath( - self.pwr_in.link().voltage, self.output_voltage, self.actual_frequency, - self.pwr_out.link().current_drawn, Range.exact(0), - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - ripple_ratio=self.ripple_ratio - )) + self.power_path = self.Block( + BuckConverterPowerPath( + self.pwr_in.link().voltage, + self.output_voltage, + self.actual_frequency, + self.pwr_out.link().current_drawn, + Range.exact(0), + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ripple_ratio=self.ripple_ratio, + ) + ) self.connect(self.power_path.pwr_in, self.pwr_in) self.connect(self.power_path.gnd, self.gnd) self.sw = self.Block(FetHalfBridge(frequency=self.frequency, fet_rds=self.rds_on)) self.connect(self.sw.gnd, self.gnd) - (self.pwr_in_force, ), _ = self.chain( # use average current draw for boundary ports - self.pwr_in, - self.Block(ForcedVoltageCurrentDraw(self.power_path.switch.link().current_drawn)), - self.sw.pwr) + (self.pwr_in_force,), _ = self.chain( # use average current draw for boundary ports + self.pwr_in, self.Block(ForcedVoltageCurrentDraw(self.power_path.switch.link().current_drawn)), self.sw.pwr + ) self.connect(self.sw.pwr_logic, self.pwr_logic) sw_ctl = self.sw.with_mixin(HalfBridgeIndependent()) self.connect(sw_ctl.low_ctl, self.pwm_low) self.connect(sw_ctl.high_ctl, self.pwm_high) - (self.sw_out_force, ), _ = self.chain( + (self.sw_out_force,), _ = self.chain( self.sw.out, # current draw used to size FETs, size for peak current self.Block(ForcedVoltageCurrentDraw(self.power_path.actual_inductor_current_peak)), - self.power_path.switch) - (self.pwr_out_force, ), _ = self.chain( + self.power_path.switch, + ) + (self.pwr_out_force,), _ = self.chain( self.power_path.pwr_out, - self.Block(ForcedVoltageCurrentLimit(self.power_path.pwr_out.current_limits.intersect( - self.sw.actual_current_limits - self.power_path.actual_inductor_current_ripple.upper() / 2).intersect( - Range.from_lower(0) - ))), - self.pwr_out + self.Block( + ForcedVoltageCurrentLimit( + self.power_path.pwr_out.current_limits.intersect( + self.sw.actual_current_limits - self.power_path.actual_inductor_current_ripple.upper() / 2 + ).intersect(Range.from_lower(0)) + ) + ), + self.pwr_out, ) diff --git a/edg/parts/BuckConverter_Mps.py b/edg/parts/BuckConverter_Mps.py index 902280340..7c74e9ac8 100644 --- a/edg/parts/BuckConverter_Mps.py +++ b/edg/parts/BuckConverter_Mps.py @@ -10,47 +10,56 @@ class Mp2722_Device(InternalSubcircuit, JlcPart, FootprintBlock): def __init__(self, charging_current: RangeLike): super().__init__() self.gnd = self.Port(Ground(), [Common]) - self.sw = self.Port(VoltageSource( - current_limits=(0, 5)*Amp # up to 5A charge / system current - )) # internal switch specs not defined, only bulk current limit defined - self.vin = self.Port(VoltageSink( - voltage_limits=(3.9, 26)*Volt, # abs max up to 26v, UV threshold up to 3.45 - current_draw=self.sw.link().current_drawn # TODO quiescent current - ), [Power]) - self.pmid = self.Port(VoltageSource( - voltage_out=self.vin.link().voltage, # 5.08-5.22v in boost - current_limits=0*Amp(tol=0) # decoupling only - )) - self.bst = self.Port(VoltageSource( - voltage_out=self.sw.link().voltage + (0, 5)*Volt, - current_limits=0*Amp(tol=0) # decoupling only - )) - - self.sys = self.Port(VoltageSink( # regulation target typically 3.7-3.94 - voltage_limits=(-0.3, 6.5)*Volt, - current_draw=charging_current # TODO model (reverse) discharge current - )) - self.batt = self.Port(VoltageSink( # technically bidir - voltage_limits=(2.6, 4.6)*Volt, # 2.6 is max UV threshold - current_draw=self.sys.link().current_drawn # TODO model (reverse) charging current - )) + self.sw = self.Port( + VoltageSource(current_limits=(0, 5) * Amp) # up to 5A charge / system current + ) # internal switch specs not defined, only bulk current limit defined + self.vin = self.Port( + VoltageSink( + voltage_limits=(3.9, 26) * Volt, # abs max up to 26v, UV threshold up to 3.45 + current_draw=self.sw.link().current_drawn, # TODO quiescent current + ), + [Power], + ) + self.pmid = self.Port( + VoltageSource( + voltage_out=self.vin.link().voltage, # 5.08-5.22v in boost + current_limits=0 * Amp(tol=0), # decoupling only + ) + ) + self.bst = self.Port( + VoltageSource( + voltage_out=self.sw.link().voltage + (0, 5) * Volt, current_limits=0 * Amp(tol=0) # decoupling only + ) + ) + + self.sys = self.Port( + VoltageSink( # regulation target typically 3.7-3.94 + voltage_limits=(-0.3, 6.5) * Volt, + current_draw=charging_current, # TODO model (reverse) discharge current + ) + ) + self.batt = self.Port( + VoltageSink( # technically bidir + voltage_limits=(2.6, 4.6) * Volt, # 2.6 is max UV threshold + current_draw=self.sys.link().current_drawn, # TODO model (reverse) charging current + ) + ) self.battsns = self.Port(VoltageSink()) # technically analog input - self.vcc = self.Port(VoltageSource( - voltage_out=3.65*Volt(tol=0), # no tolerance given - current_limits=(0, 5)*mAmp # no limit given, can be used to drive stat LEDs - )) + self.vcc = self.Port( + VoltageSource( + voltage_out=3.65 * Volt(tol=0), # no tolerance given + current_limits=(0, 5) * mAmp, # no limit given, can be used to drive stat LEDs + ) + ) # DIO valid for SCL, SDA, INT, RST, STAT - dio_model = DigitalBidir.from_supply(self.gnd, self.vcc, - voltage_limit_abs=(-0.3, 5)*Volt, - input_threshold_abs=(0.4, 1.3)*Volt) + dio_model = DigitalBidir.from_supply( + self.gnd, self.vcc, voltage_limit_abs=(-0.3, 5) * Volt, input_threshold_abs=(0.4, 1.3) * Volt + ) self.rst = self.Port(dio_model, optional=True) # 200k internal pullup, float if unused self.int = self.Port(DigitalSource.low_from_supply(self.gnd), optional=True) - self.vrntc = self.Port(VoltageSource( - voltage_out=self.vcc.voltage_out, - current_limits=(0, 5)*mAmp - )) + self.vrntc = self.Port(VoltageSource(voltage_out=self.vcc.voltage_out, current_limits=(0, 5) * mAmp)) self.ntc1 = self.Port(AnalogSink()) # required, doesn't seem to be any way to disable self.stat = self.Port(DigitalSource.low_from_supply(self.gnd), optional=True) # requires 10k pullup self.pg = self.Port(DigitalSource.low_from_supply(self.gnd), optional=True) # requires 10k pullup @@ -63,38 +72,40 @@ def __init__(self, charging_current: RangeLike): @override def contents(self) -> None: super().contents() - self.require(self.vin.current_draw.within((0, 3.2)*Amp), "Iin max limit") + self.require(self.vin.current_draw.within((0, 3.2) * Amp), "Iin max limit") self.footprint( - 'U', 'edg:MPS_QFN-22_2.5x3.5mm_P0.4mm', + "U", + "edg:MPS_QFN-22_2.5x3.5mm_P0.4mm", { - '2': self.vin, - '3': self.pmid, - '4': self.sw, - '6': self.bst, - '13': self.sys, - '14': self.batt, - '5': self.gnd, # PGND - '18': self.gnd, # AGND - '19': self.vcc, - '12': self.battsns, - '8': self.int, - '16': self.i2c.scl, - '15': self.i2c.sda, - '1': self.cc.cc1, - '22': self.cc.cc2, - '21': self.usb.dp, - '20': self.usb.dm, - '17': self.rst, - '7': self.vrntc, # powered to Vcc when in operation - '10': self.ntc1, - '9': self.pg, - '11': self.stat, + "2": self.vin, + "3": self.pmid, + "4": self.sw, + "6": self.bst, + "13": self.sys, + "14": self.batt, + "5": self.gnd, # PGND + "18": self.gnd, # AGND + "19": self.vcc, + "12": self.battsns, + "8": self.int, + "16": self.i2c.scl, + "15": self.i2c.sda, + "1": self.cc.cc1, + "22": self.cc.cc2, + "21": self.usb.dp, + "20": self.usb.dm, + "17": self.rst, + "7": self.vrntc, # powered to Vcc when in operation + "10": self.ntc1, + "9": self.pg, + "11": self.stat, }, - mfr='Monolithic Power Systems Inc.', part='MP2722GRH-0000-P', - datasheet='https://www.monolithicpower.com/en/documentview/productdocument/index/version/2/document_type/Datasheet/lang/en/sku/MP2722GRH/document_id/10035/' + mfr="Monolithic Power Systems Inc.", + part="MP2722GRH-0000-P", + datasheet="https://www.monolithicpower.com/en/documentview/productdocument/index/version/2/document_type/Datasheet/lang/en/sku/MP2722GRH/document_id/10035/", ) - self.assign(self.lcsc_part, 'C20099550') + self.assign(self.lcsc_part, "C20099550") self.assign(self.actual_basic_part, False) @@ -104,8 +115,13 @@ class Mp2722(DiscreteBuckConverter): VSYS_MIN_DEFAULT = 3.588 # regulation target, tracks above this - def __init__(self, *args: Any, frequency: RangeLike = (900, 1280)*kHertz, - charging_current: RangeLike = (0, 3)*Amp, **kwargs: Any): + def __init__( + self, + *args: Any, + frequency: RangeLike = (900, 1280) * kHertz, + charging_current: RangeLike = (0, 3) * Amp, + **kwargs: Any, + ): super().__init__(*args, **kwargs) self.ic = self.Block(Mp2722_Device(charging_current)) @@ -136,36 +152,45 @@ def contents(self) -> None: super().contents() # TODO only allow subset of frequencies, based on SW_FREQ table - self.require(self.frequency.within((630, 1680)*kHertz)) + self.require(self.frequency.within((630, 1680) * kHertz)) self.assign(self.actual_frequency, self.frequency) self.connect(self.ic.batt, self.ic.battsns) # TODO allow remote sense with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.gnd, [Common]), ) as imp: - self.vbst_cap = self.Block(Capacitor(capacitance=22*nFarad(tol=0.2), voltage=(0, 5)*Volt)) + self.vbst_cap = self.Block(Capacitor(capacitance=22 * nFarad(tol=0.2), voltage=(0, 5) * Volt)) self.connect(self.vbst_cap.neg.adapt_to(VoltageSink()), self.ic.sw) self.connect(self.vbst_cap.pos.adapt_to(VoltageSink()), self.ic.bst) # decouple to PMID, BATT to PGND - self.pmid_cap = imp.Block(DecouplingCapacitor((10 * 0.8, float('inf'))*uFarad)).connected(pwr=self.ic.pmid) - self.batt_cap = imp.Block(DecouplingCapacitor((20 * 0.8, float('inf'))*uFarad)).connected(pwr=self.ic.batt) + self.pmid_cap = imp.Block(DecouplingCapacitor((10 * 0.8, float("inf")) * uFarad)).connected( + pwr=self.ic.pmid + ) + self.batt_cap = imp.Block(DecouplingCapacitor((20 * 0.8, float("inf")) * uFarad)).connected( + pwr=self.ic.batt + ) # decouple to AGND - self.vcc_cap = imp.Block(DecouplingCapacitor(4.7*uFarad(tol=0.2))).connected(pwr=self.ic.vcc) + self.vcc_cap = imp.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(pwr=self.ic.vcc) vsys_range = (self.VSYS_MIN_DEFAULT + 0.15, self.batt.link().voltage.upper()) - self.power_path = imp.Block(BuckConverterPowerPath( - self.pwr_in.link().voltage, vsys_range, self.actual_frequency, - self.pwr_out.link().current_drawn + self.ic.sys.link().current_drawn, (0, 3.2)*Amp, - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit - )) + self.power_path = imp.Block( + BuckConverterPowerPath( + self.pwr_in.link().voltage, + vsys_range, + self.actual_frequency, + self.pwr_out.link().current_drawn + self.ic.sys.link().current_drawn, + (0, 3.2) * Amp, + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ) + ) self.connect(self.power_path.pwr_in, self.pwr_in) # ForcedVoltage needed to provide a voltage value so current downstream can be calculated # and then the power path can generate - (self.forced_out, ), _ = self.chain(self.power_path.pwr_out, - self.Block(ForcedVoltage(vsys_range)), - self.pwr_out) + (self.forced_out,), _ = self.chain( + self.power_path.pwr_out, self.Block(ForcedVoltage(vsys_range)), self.pwr_out + ) self.connect(self.power_path.switch, self.ic.sw) diff --git a/edg/parts/BuckConverter_TexasInstruments.py b/edg/parts/BuckConverter_TexasInstruments.py index a7b8eda6b..6699fd6ca 100644 --- a/edg/parts/BuckConverter_TexasInstruments.py +++ b/edg/parts/BuckConverter_TexasInstruments.py @@ -5,179 +5,205 @@ class Tps561201_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.sw = self.Port(VoltageSource()) # internal switch specs not defined, only bulk current limit defined - self.pwr_in = self.Port(VoltageSink( - voltage_limits=(4.5, 17)*Volt, - current_draw=self.sw.link().current_drawn # TODO quiescent current - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - self.fb = self.Port(AnalogSink(impedance=(8000, float('inf')) * kOhm)) # based on input current spec - self.vbst = self.Port(VoltageSource()) - self.en = self.Port(DigitalSink( - voltage_limits=(-0.1, 17)*Volt, - input_thresholds=(0.8, 1.6)*Volt - )) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', - { - '1': self.gnd, - '2': self.sw, - '3': self.pwr_in, - '4': self.fb, - '5': self.en, - '6': self.vbst, - }, - mfr='Texas Instruments', part='TPS561201', - datasheet='https://www.ti.com/lit/ds/symlink/tps561201.pdf' - ) - self.assign(self.lcsc_part, 'C220433') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + self.sw = self.Port(VoltageSource()) # internal switch specs not defined, only bulk current limit defined + self.pwr_in = self.Port( + VoltageSink( + voltage_limits=(4.5, 17) * Volt, current_draw=self.sw.link().current_drawn # TODO quiescent current + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + self.fb = self.Port(AnalogSink(impedance=(8000, float("inf")) * kOhm)) # based on input current spec + self.vbst = self.Port(VoltageSource()) + self.en = self.Port(DigitalSink(voltage_limits=(-0.1, 17) * Volt, input_thresholds=(0.8, 1.6) * Volt)) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-6", + { + "1": self.gnd, + "2": self.sw, + "3": self.pwr_in, + "4": self.fb, + "5": self.en, + "6": self.vbst, + }, + mfr="Texas Instruments", + part="TPS561201", + datasheet="https://www.ti.com/lit/ds/symlink/tps561201.pdf", + ) + self.assign(self.lcsc_part, "C220433") + self.assign(self.actual_basic_part, False) class Tps561201(VoltageRegulatorEnableWrapper, DiscreteBuckConverter): - """Adjustable synchronous buck converter in SOT-23-6 with integrated switch""" - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.en - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.actual_frequency, 580*kHertz(tol=0)) - - with self.implicit_connect( - ImplicitConnect(self.pwr_in, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Tps561201_Device()) - - self.fb = imp.Block(FeedbackVoltageDivider( - output_voltage=(0.749, 0.787) * Volt, - impedance=(1, 10) * kOhm, - assumed_input_voltage=self.output_voltage - )) - self.connect(self.fb.input, self.pwr_out) - self.connect(self.fb.output, self.ic.fb) - - self.hf_in_cap = imp.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2))) # Datasheet 8.2.2.4 - - self.vbst_cap = self.Block(Capacitor(capacitance=0.1*uFarad(tol=0.2), voltage=(0, 6) * Volt)) - self.connect(self.vbst_cap.neg.adapt_to(VoltageSink()), self.ic.sw) - self.connect(self.vbst_cap.pos.adapt_to(VoltageSink()), self.ic.vbst) - - # TODO: the control mechanism requires a specific capacitor / inductor selection, datasheet 8.2.2.3 - self.power_path = imp.Block(BuckConverterPowerPath( - self.pwr_in.link().voltage, self.fb.actual_input_voltage, self.actual_frequency, - self.pwr_out.link().current_drawn, (0, 1.2)*Amp, # output current limit, switch limit not given - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - )) - # ForcedVoltage needed to provide a voltage value so current downstream can be calculated - # and then the power path can generate - (self.forced_out, ), _ = self.chain(self.power_path.pwr_out, - self.Block(ForcedVoltage(self.fb.actual_input_voltage)), - self.pwr_out) - self.connect(self.power_path.switch, self.ic.sw) + """Adjustable synchronous buck converter in SOT-23-6 with integrated switch""" + + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.en + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.actual_frequency, 580 * kHertz(tol=0)) + + with self.implicit_connect( + ImplicitConnect(self.pwr_in, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Tps561201_Device()) + + self.fb = imp.Block( + FeedbackVoltageDivider( + output_voltage=(0.749, 0.787) * Volt, + impedance=(1, 10) * kOhm, + assumed_input_voltage=self.output_voltage, + ) + ) + self.connect(self.fb.input, self.pwr_out) + self.connect(self.fb.output, self.ic.fb) + + self.hf_in_cap = imp.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))) # Datasheet 8.2.2.4 + + self.vbst_cap = self.Block(Capacitor(capacitance=0.1 * uFarad(tol=0.2), voltage=(0, 6) * Volt)) + self.connect(self.vbst_cap.neg.adapt_to(VoltageSink()), self.ic.sw) + self.connect(self.vbst_cap.pos.adapt_to(VoltageSink()), self.ic.vbst) + + # TODO: the control mechanism requires a specific capacitor / inductor selection, datasheet 8.2.2.3 + self.power_path = imp.Block( + BuckConverterPowerPath( + self.pwr_in.link().voltage, + self.fb.actual_input_voltage, + self.actual_frequency, + self.pwr_out.link().current_drawn, + (0, 1.2) * Amp, # output current limit, switch limit not given + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ) + ) + # ForcedVoltage needed to provide a voltage value so current downstream can be calculated + # and then the power path can generate + (self.forced_out,), _ = self.chain( + self.power_path.pwr_out, self.Block(ForcedVoltage(self.fb.actual_input_voltage)), self.pwr_out + ) + self.connect(self.power_path.switch, self.ic.sw) class Tps54202h_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.sw = self.Port(VoltageSource( - current_limits=(0, 2)*Amp # most conservative figures, low-side limited. TODO: better ones? - )) # internal switch specs not defined, only bulk current limit defined - self.pwr_in = self.Port(VoltageSink( - voltage_limits=(4.5, 28)*Volt, - current_draw=self.sw.link().current_drawn # TODO quiescent current - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - self.fb = self.Port(AnalogSink()) # no impedance specs - self.boot = self.Port(VoltageSource()) - self.en = self.Port(DigitalSink( # must be connected, floating is disable - voltage_limits=(-0.1, 7) * Volt, - input_thresholds=(1.16, 1.35)*Volt - )) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', - { - '1': self.gnd, - '2': self.sw, - '3': self.pwr_in, - '4': self.fb, - '5': self.en, - '6': self.boot, - }, - mfr='Texas Instruments', part='TPS54202H', - datasheet='https://www.ti.com/lit/ds/symlink/tps54202h.pdf' - ) - self.assign(self.lcsc_part, 'C527684') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + self.sw = self.Port( + VoltageSource( + current_limits=(0, 2) * Amp # most conservative figures, low-side limited. TODO: better ones? + ) + ) # internal switch specs not defined, only bulk current limit defined + self.pwr_in = self.Port( + VoltageSink( + voltage_limits=(4.5, 28) * Volt, current_draw=self.sw.link().current_drawn # TODO quiescent current + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + self.fb = self.Port(AnalogSink()) # no impedance specs + self.boot = self.Port(VoltageSource()) + self.en = self.Port( + DigitalSink( # must be connected, floating is disable + voltage_limits=(-0.1, 7) * Volt, input_thresholds=(1.16, 1.35) * Volt + ) + ) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-6", + { + "1": self.gnd, + "2": self.sw, + "3": self.pwr_in, + "4": self.fb, + "5": self.en, + "6": self.boot, + }, + mfr="Texas Instruments", + part="TPS54202H", + datasheet="https://www.ti.com/lit/ds/symlink/tps54202h.pdf", + ) + self.assign(self.lcsc_part, "C527684") + self.assign(self.actual_basic_part, False) class Tps54202h(Resettable, DiscreteBuckConverter, GeneratorBlock): - """Adjustable synchronous buck converter in SOT-23-6 with integrated switch, 4.5-24v capable - Note: TPS54202 has frequency spread-spectrum operation and internal pull-up on EN - TPS54202H has no internal EN pull-up but a Zener diode clamp to limit voltage. - """ - @override - def contents(self) -> None: - super().contents() - self.generator_param(self.reset.is_connected()) - - self.assign(self.actual_frequency, (390, 590)*kHertz) - - with self.implicit_connect( - ImplicitConnect(self.pwr_in, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Tps54202h_Device()) - - self.fb = imp.Block(FeedbackVoltageDivider( - output_voltage=(0.581, 0.611) * Volt, - impedance=(1, 10) * kOhm, - assumed_input_voltage=self.output_voltage - )) - self.connect(self.fb.input, self.pwr_out) - self.connect(self.fb.output, self.ic.fb) - - self.hf_in_cap = imp.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2))) # Datasheet 8.2.3.1, "optional"? - - self.boot_cap = self.Block(Capacitor(capacitance=0.1*uFarad(tol=0.2), voltage=(0, 6) * Volt)) - self.connect(self.boot_cap.neg.adapt_to(VoltageSink()), self.ic.sw) - self.connect(self.boot_cap.pos.adapt_to(VoltageSink()), self.ic.boot) - - self.power_path = imp.Block(BuckConverterPowerPath( - self.pwr_in.link().voltage, self.fb.actual_input_voltage, self.actual_frequency, - self.pwr_out.link().current_drawn, (0, 2.5)*Amp, - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - )) - # ForcedVoltage needed to provide a voltage value so current downstream can be calculated - # and then the power path can generate - (self.forced_out, ), _ = self.chain(self.power_path.pwr_out, - self.Block(ForcedVoltage(self.fb.actual_input_voltage)), - self.pwr_out) - self.connect(self.power_path.switch, self.ic.sw) - - @override - def generate(self) -> None: - super().generate() - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.en) - else: # by default tie high to enable regulator - # an internal 6.9v Zener clamps the enable voltage, datasheet recommends at 510k resistor - # a pull-up resistor isn't used because - self.en_res = self.Block(Resistor(resistance=510*kOhm(tol=0.05), power=0*Amp(tol=0))) - self.connect(self.pwr_in, self.en_res.a.adapt_to(VoltageSink())) - self.connect(self.en_res.b.adapt_to(DigitalSource()), self.ic.en) + """Adjustable synchronous buck converter in SOT-23-6 with integrated switch, 4.5-24v capable + Note: TPS54202 has frequency spread-spectrum operation and internal pull-up on EN + TPS54202H has no internal EN pull-up but a Zener diode clamp to limit voltage. + """ + + @override + def contents(self) -> None: + super().contents() + self.generator_param(self.reset.is_connected()) + + self.assign(self.actual_frequency, (390, 590) * kHertz) + + with self.implicit_connect( + ImplicitConnect(self.pwr_in, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Tps54202h_Device()) + + self.fb = imp.Block( + FeedbackVoltageDivider( + output_voltage=(0.581, 0.611) * Volt, + impedance=(1, 10) * kOhm, + assumed_input_voltage=self.output_voltage, + ) + ) + self.connect(self.fb.input, self.pwr_out) + self.connect(self.fb.output, self.ic.fb) + + self.hf_in_cap = imp.Block( + DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2)) + ) # Datasheet 8.2.3.1, "optional"? + + self.boot_cap = self.Block(Capacitor(capacitance=0.1 * uFarad(tol=0.2), voltage=(0, 6) * Volt)) + self.connect(self.boot_cap.neg.adapt_to(VoltageSink()), self.ic.sw) + self.connect(self.boot_cap.pos.adapt_to(VoltageSink()), self.ic.boot) + + self.power_path = imp.Block( + BuckConverterPowerPath( + self.pwr_in.link().voltage, + self.fb.actual_input_voltage, + self.actual_frequency, + self.pwr_out.link().current_drawn, + (0, 2.5) * Amp, + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + ) + ) + # ForcedVoltage needed to provide a voltage value so current downstream can be calculated + # and then the power path can generate + (self.forced_out,), _ = self.chain( + self.power_path.pwr_out, self.Block(ForcedVoltage(self.fb.actual_input_voltage)), self.pwr_out + ) + self.connect(self.power_path.switch, self.ic.sw) + + @override + def generate(self) -> None: + super().generate() + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.en) + else: # by default tie high to enable regulator + # an internal 6.9v Zener clamps the enable voltage, datasheet recommends at 510k resistor + # a pull-up resistor isn't used because + self.en_res = self.Block(Resistor(resistance=510 * kOhm(tol=0.05), power=0 * Amp(tol=0))) + self.connect(self.pwr_in, self.en_res.a.adapt_to(VoltageSink())) + self.connect(self.en_res.b.adapt_to(DigitalSource()), self.ic.en) diff --git a/edg/parts/Camera_Ov2640_Fpc24.py b/edg/parts/Camera_Ov2640_Fpc24.py index 9b12bf7b7..8cdf57e91 100644 --- a/edg/parts/Camera_Ov2640_Fpc24.py +++ b/edg/parts/Camera_Ov2640_Fpc24.py @@ -10,60 +10,75 @@ def __init__(self) -> None: self.conn = self.Block(Fpc050Bottom(length=24)) - self.dgnd = self.Export(self.conn.pins.request('10').adapt_to(Ground())) - self.agnd = self.Export(self.conn.pins.request('23').adapt_to(Ground())) - self.dovdd = self.Export(self.conn.pins.request('14').adapt_to(VoltageSink( - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (1.71, 4.5)*Volt, # Table 6, absolute maximum (Table 5) is 4.5v - (1.71, 3.3)*Volt), - current_draw=(6, 15)*mAmp # active typ to max - ))) - self.dvdd = self.Export(self.conn.pins.request('15').adapt_to(VoltageSink( - voltage_limits=(1.14, 1.26)*Volt, # Table 6 - current_draw=(30, 60)*mAmp # active typ YUV to max compressed - ))) - self.avdd = self.Export(self.conn.pins.request('21').adapt_to(VoltageSink( - voltage_limits=(2.5, 3.0)*Volt, # Table 6, absolute maximum (Table 5) is 4.5v - current_draw=(30, 40)*mAmp # active max - ))) - - dio_model = DigitalBidir.from_supply(self.dgnd, self.dovdd, - voltage_limit_tolerance=(-0.3, 1)*Volt, # Table 5 - input_threshold_abs=(0.54, 1.26)*Volt, - ) + self.dgnd = self.Export(self.conn.pins.request("10").adapt_to(Ground())) + self.agnd = self.Export(self.conn.pins.request("23").adapt_to(Ground())) + self.dovdd = self.Export( + self.conn.pins.request("14").adapt_to( + VoltageSink( + voltage_limits=self.nonstrict_3v3_compatible.then_else( + (1.71, 4.5) * Volt, (1.71, 3.3) * Volt # Table 6, absolute maximum (Table 5) is 4.5v + ), + current_draw=(6, 15) * mAmp, # active typ to max + ) + ) + ) + self.dvdd = self.Export( + self.conn.pins.request("15").adapt_to( + VoltageSink( + voltage_limits=(1.14, 1.26) * Volt, # Table 6 + current_draw=(30, 60) * mAmp, # active typ YUV to max compressed + ) + ) + ) + self.avdd = self.Export( + self.conn.pins.request("21").adapt_to( + VoltageSink( + voltage_limits=(2.5, 3.0) * Volt, # Table 6, absolute maximum (Table 5) is 4.5v + current_draw=(30, 40) * mAmp, # active max + ) + ) + ) + + dio_model = DigitalBidir.from_supply( + self.dgnd, + self.dovdd, + voltage_limit_tolerance=(-0.3, 1) * Volt, # Table 5 + input_threshold_abs=(0.54, 1.26) * Volt, + ) do_model = DigitalSource.from_bidir(dio_model) di_model = DigitalSink.from_bidir(dio_model) self.y = self.Port(Vector(DigitalSource.empty())) - self.connect(self.y.append_elt(DigitalSource.empty(), '0'), self.conn.pins.request('1').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '1'), self.conn.pins.request('2').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '4'), self.conn.pins.request('3').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '3'), self.conn.pins.request('4').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '5'), self.conn.pins.request('5').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '2'), self.conn.pins.request('6').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '6'), self.conn.pins.request('7').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '7'), self.conn.pins.request('9').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '8'), self.conn.pins.request('11').adapt_to(do_model)) - self.connect(self.y.append_elt(DigitalSource.empty(), '9'), self.conn.pins.request('13').adapt_to(do_model)) - - self.pclk = self.Export(self.conn.pins.request('8').adapt_to(do_model)) # tacked on a 15pF cap - self.xclk = self.Export(self.conn.pins.request('12').adapt_to(di_model)) - self.href = self.Export(self.conn.pins.request('16').adapt_to(do_model)) - self.pwdn = self.Export(self.conn.pins.request('17').adapt_to(di_model)) # typically pulled down / grounded - self.vsync = self.Export(self.conn.pins.request('18').adapt_to(do_model)) - self.reset = self.Export(self.conn.pins.request('19').adapt_to(di_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "0"), self.conn.pins.request("1").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "1"), self.conn.pins.request("2").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "4"), self.conn.pins.request("3").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "3"), self.conn.pins.request("4").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "5"), self.conn.pins.request("5").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "2"), self.conn.pins.request("6").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "6"), self.conn.pins.request("7").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "7"), self.conn.pins.request("9").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "8"), self.conn.pins.request("11").adapt_to(do_model)) + self.connect(self.y.append_elt(DigitalSource.empty(), "9"), self.conn.pins.request("13").adapt_to(do_model)) + + self.pclk = self.Export(self.conn.pins.request("8").adapt_to(do_model)) # tacked on a 15pF cap + self.xclk = self.Export(self.conn.pins.request("12").adapt_to(di_model)) + self.href = self.Export(self.conn.pins.request("16").adapt_to(do_model)) + self.pwdn = self.Export(self.conn.pins.request("17").adapt_to(di_model)) # typically pulled down / grounded + self.vsync = self.Export(self.conn.pins.request("18").adapt_to(do_model)) + self.reset = self.Export(self.conn.pins.request("19").adapt_to(di_model)) # formally this is SCCB (serial camera control bus), but is I2C compatible # https://e2e.ti.com/support/processors-group/processors/f/processors-forum/6092/sccb-vs-i2c # 0x60 for write, 0x61 for read, translated to the 7-bit address self.sio = self.Port(I2cTarget(DigitalBidir.empty(), [0x30])) - self.connect(self.sio.scl, self.conn.pins.request('20').adapt_to(dio_model)) - self.connect(self.sio.sda, self.conn.pins.request('22').adapt_to(dio_model)) + self.connect(self.sio.scl, self.conn.pins.request("20").adapt_to(dio_model)) + self.connect(self.sio.sda, self.conn.pins.request("22").adapt_to(dio_model)) @abstract_block_default(lambda: Ov2640_Fpc24) class Ov2640(Camera, Block): """OV2640 digital camera with DVP interface, commonly used with ESP32 devices""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Ov2640_Fpc24_Device()) @@ -86,6 +101,7 @@ class Ov2640_Fpc24(Ov2640, GeneratorBlock): Pinning and interface circuit from https://github.com/Freenove/Freenove_ESP32_WROVER_Board/blob/f710fd6976e76ab76c29c2ee3042cd7bac22c3d6/Datasheet/ESP32_Schematic.pdf and https://www.waveshare.com/w/upload/9/99/OV2640-Camera-Board-Schematic.pdf On many boards, Y0 and Y1 (LSBs) are left unconnected to save IOs.""" + def __init__(self) -> None: super().__init__() self.generator_param(self.pwdn.is_connected(), self.reset.is_connected()) @@ -93,27 +109,28 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.dovdd_cap = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2)))\ - .connected(self.device.dgnd, self.device.dovdd) + self.dovdd_cap = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected( + self.device.dgnd, self.device.dovdd + ) - self.reset_cap = self.Block(Capacitor(capacitance=0.1*uFarad(tol=0.2), voltage=self.pwr.link().voltage)) + self.reset_cap = self.Block(Capacitor(capacitance=0.1 * uFarad(tol=0.2), voltage=self.pwr.link().voltage)) self.connect(self.reset_cap.neg.adapt_to(Ground()), self.gnd) self.connect(self.reset_cap.pos.adapt_to(DigitalSink()), self.device.reset) self.connect(self.dvp8.xclk, self.device.xclk) - self.pclk_cap = self.Block(Capacitor(capacitance=15*pFarad(tol=0.2), voltage=self.device.pclk.link().voltage)) + self.pclk_cap = self.Block(Capacitor(capacitance=15 * pFarad(tol=0.2), voltage=self.device.pclk.link().voltage)) self.connect(self.pclk_cap.neg.adapt_to(Ground()), self.gnd) self.connect(self.dvp8.pclk, self.pclk_cap.pos.adapt_to(DigitalSink()), self.device.pclk) self.connect(self.dvp8.href, self.device.href) self.connect(self.dvp8.vsync, self.device.vsync) - self.connect(self.dvp8.y0, self.device.y.request('2')) - self.connect(self.dvp8.y1, self.device.y.request('3')) - self.connect(self.dvp8.y2, self.device.y.request('4')) - self.connect(self.dvp8.y3, self.device.y.request('5')) - self.connect(self.dvp8.y4, self.device.y.request('6')) - self.connect(self.dvp8.y5, self.device.y.request('7')) - self.connect(self.dvp8.y6, self.device.y.request('8')) - self.connect(self.dvp8.y7, self.device.y.request('9')) + self.connect(self.dvp8.y0, self.device.y.request("2")) + self.connect(self.dvp8.y1, self.device.y.request("3")) + self.connect(self.dvp8.y2, self.device.y.request("4")) + self.connect(self.dvp8.y3, self.device.y.request("5")) + self.connect(self.dvp8.y4, self.device.y.request("6")) + self.connect(self.dvp8.y5, self.device.y.request("7")) + self.connect(self.dvp8.y6, self.device.y.request("8")) + self.connect(self.dvp8.y7, self.device.y.request("9")) @override def generate(self) -> None: @@ -126,4 +143,4 @@ def generate(self) -> None: if self.get(self.reset.is_connected()): self.connect(self.reset, self.device.reset) else: - self.reset_pull = self.Block(PullupResistor(10*kOhm(tol=0.05))).connected(self.pwr, self.device.reset) + self.reset_pull = self.Block(PullupResistor(10 * kOhm(tol=0.05))).connected(self.pwr, self.device.reset) diff --git a/edg/parts/CanBlocks.py b/edg/parts/CanBlocks.py index b4d7d5f63..92f9fa5c5 100644 --- a/edg/parts/CanBlocks.py +++ b/edg/parts/CanBlocks.py @@ -4,21 +4,23 @@ class Pesd1can(CanEsdDiode, FootprintBlock): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.gnd.init_from(Ground()) - self.can.init_from(CanDiffPort()) + self.gnd.init_from(Ground()) + self.can.init_from(CanDiffPort()) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', - { - '1': self.can.canl, # TODO technically 1, 2 can be swapped - '2': self.can.canh, - '3': self.gnd, - }, - part='PESD1CAN,215') + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23", + { + "1": self.can.canl, # TODO technically 1, 2 can be swapped + "2": self.can.canh, + "3": self.gnd, + }, + part="PESD1CAN,215", + ) diff --git a/edg/parts/CanTransceiver_Iso1050.py b/edg/parts/CanTransceiver_Iso1050.py index 1ea5debcb..de1753d21 100644 --- a/edg/parts/CanTransceiver_Iso1050.py +++ b/edg/parts/CanTransceiver_Iso1050.py @@ -4,69 +4,80 @@ class Iso1050dub_Device(InternalSubcircuit, FootprintBlock): - def __init__(self) -> None: - super().__init__() - # Table 6.3, recommended operating conditions - self.vcc1 = self.Port(VoltageSink( - voltage_limits=(3, 5.5) * Volt, current_draw=(0, 3.6) * mAmp - )) - self.vcc2 = self.Port(VoltageSink( - voltage_limits=(4.75, 5.25) * Volt, current_draw=(0, 73)*mAmp - )) - self.gnd1 = self.Port(Ground()) - self.gnd2 = self.Port(Ground()) + def __init__(self) -> None: + super().__init__() + # Table 6.3, recommended operating conditions + self.vcc1 = self.Port(VoltageSink(voltage_limits=(3, 5.5) * Volt, current_draw=(0, 3.6) * mAmp)) + self.vcc2 = self.Port(VoltageSink(voltage_limits=(4.75, 5.25) * Volt, current_draw=(0, 73) * mAmp)) + self.gnd1 = self.Port(Ground()) + self.gnd2 = self.Port(Ground()) - self.controller = self.Port(CanTransceiverPort(DigitalBidir( - voltage_limits=(-0.5 * Volt, self.vcc1.link().voltage.lower() + 0.5 * Volt), - voltage_out=(0 * Volt, self.vcc1.link().voltage.lower()), - current_limits=(-5, 5) * uAmp, - input_thresholds=(0.8, 2) * Volt, - output_thresholds=(0 * Volt, self.vcc1.link().voltage.lower()) - ))) + self.controller = self.Port( + CanTransceiverPort( + DigitalBidir( + voltage_limits=(-0.5 * Volt, self.vcc1.link().voltage.lower() + 0.5 * Volt), + voltage_out=(0 * Volt, self.vcc1.link().voltage.lower()), + current_limits=(-5, 5) * uAmp, + input_thresholds=(0.8, 2) * Volt, + output_thresholds=(0 * Volt, self.vcc1.link().voltage.lower()), + ) + ) + ) - self.can = self.Port(CanDiffPort(DigitalBidir( - voltage_limits=(-7, 7) * Volt, # TODO: need better model of differential pins where there can be a common-mode offset - voltage_out=(0.8, 4.5) * Volt, - current_draw=(-4, 4) * mAmp, current_limits=(-70, 70) * mAmp - ))) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOP-8_6.62x9.15mm_P2.54mm', - { - '1': self.vcc1, - '2': self.controller.rxd, - '3': self.controller.txd, - '4': self.gnd1, - '5': self.gnd2, - '6': self.can.canl, - '7': self.can.canh, - '8': self.vcc2, - }, - mfr='Texas Instruments', part='ISO1050DUB', - datasheet='https://www.ti.com/lit/ds/symlink/iso1050.pdf' - ) + self.can = self.Port( + CanDiffPort( + DigitalBidir( + voltage_limits=(-7, 7) + * Volt, # TODO: need better model of differential pins where there can be a common-mode offset + voltage_out=(0.8, 4.5) * Volt, + current_draw=(-4, 4) * mAmp, + current_limits=(-70, 70) * mAmp, + ) + ) + ) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOP-8_6.62x9.15mm_P2.54mm", + { + "1": self.vcc1, + "2": self.controller.rxd, + "3": self.controller.txd, + "4": self.gnd1, + "5": self.gnd2, + "6": self.can.canl, + "7": self.can.canh, + "8": self.vcc2, + }, + mfr="Texas Instruments", + part="ISO1050DUB", + datasheet="https://www.ti.com/lit/ds/symlink/iso1050.pdf", + ) class Iso1050dub(IsolatedCanTransceiver): - @override - def contents(self) -> None: - super().contents() - self.ic = self.Block(Iso1050dub_Device()) - self.connect(self.ic.controller, self.controller) - self.connect(self.ic.can, self.can) + @override + def contents(self) -> None: + super().contents() + self.ic = self.Block(Iso1050dub_Device()) + self.connect(self.ic.controller, self.controller) + self.connect(self.ic.can, self.can) - self.logic_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )) - self.connect(self.pwr, self.ic.vcc1, self.logic_cap.pwr) - self.connect(self.gnd, self.ic.gnd1, self.logic_cap.gnd) + self.logic_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ) + self.connect(self.pwr, self.ic.vcc1, self.logic_cap.pwr) + self.connect(self.gnd, self.ic.gnd1, self.logic_cap.gnd) - self.can_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )) - self.connect(self.can_pwr, self.ic.vcc2, self.can_cap.pwr) - self.connect(self.can_gnd, self.ic.gnd2, self.can_cap.gnd) + self.can_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ) + self.connect(self.can_pwr, self.ic.vcc2, self.can_cap.pwr) + self.connect(self.can_gnd, self.ic.gnd2, self.can_cap.gnd) diff --git a/edg/parts/CanTransceiver_Sn65hvd230.py b/edg/parts/CanTransceiver_Sn65hvd230.py index bb5745f83..d7cc2296b 100644 --- a/edg/parts/CanTransceiver_Sn65hvd230.py +++ b/edg/parts/CanTransceiver_Sn65hvd230.py @@ -5,59 +5,70 @@ class Sn65hvd230_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vcc = self.Port(VoltageSink( - voltage_limits=(3, 3.6) * Volt, current_draw=(0.370, 17) * mAmp - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - - self.controller = self.Port(CanTransceiverPort(DigitalBidir( - voltage_limits=(-0.5 * Volt, self.vcc.link().voltage.lower() + 0.5 * Volt), - voltage_out=(0 * Volt, self.vcc.link().voltage.lower()), - current_limits=(-8, 8) * mAmp, # driver pin actually -40-48mA - input_thresholds=(0.8, 2) * Volt, - output_thresholds=(0 * Volt, self.vcc.link().voltage.lower()) - ))) - - self.can = self.Port(CanDiffPort(DigitalBidir( - voltage_limits=(-2.5, 7.5) * Volt, - voltage_out=(0.5 * Volt, self.vcc.link().voltage.lower()), - current_draw=(-30, 30) * uAmp, current_limits=(-250, 250) * mAmp - ))) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.controller.txd, - '2': self.gnd, - '3': self.vcc, - '4': self.controller.rxd, - # '5': , # Vref = Vcc/2 output pin - '6': self.can.canl, - '7': self.can.canh, - '8': self.gnd, # Gs, set to GND for high speed mode - }, - mfr='Texas Instruments', part='SN65HVD230DR', - datasheet='www.ti.com/lit/ds/symlink/sn65hvd230.pdf' - ) - self.assign(self.lcsc_part, 'C12084') - self.assign(self.actual_basic_part, True) + def __init__(self) -> None: + super().__init__() + self.vcc = self.Port(VoltageSink(voltage_limits=(3, 3.6) * Volt, current_draw=(0.370, 17) * mAmp), [Power]) + self.gnd = self.Port(Ground(), [Common]) + + self.controller = self.Port( + CanTransceiverPort( + DigitalBidir( + voltage_limits=(-0.5 * Volt, self.vcc.link().voltage.lower() + 0.5 * Volt), + voltage_out=(0 * Volt, self.vcc.link().voltage.lower()), + current_limits=(-8, 8) * mAmp, # driver pin actually -40-48mA + input_thresholds=(0.8, 2) * Volt, + output_thresholds=(0 * Volt, self.vcc.link().voltage.lower()), + ) + ) + ) + + self.can = self.Port( + CanDiffPort( + DigitalBidir( + voltage_limits=(-2.5, 7.5) * Volt, + voltage_out=(0.5 * Volt, self.vcc.link().voltage.lower()), + current_draw=(-30, 30) * uAmp, + current_limits=(-250, 250) * mAmp, + ) + ) + ) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.controller.txd, + "2": self.gnd, + "3": self.vcc, + "4": self.controller.rxd, + # '5': , # Vref = Vcc/2 output pin + "6": self.can.canl, + "7": self.can.canh, + "8": self.gnd, # Gs, set to GND for high speed mode + }, + mfr="Texas Instruments", + part="SN65HVD230DR", + datasheet="www.ti.com/lit/ds/symlink/sn65hvd230.pdf", + ) + self.assign(self.lcsc_part, "C12084") + self.assign(self.actual_basic_part, True) class Sn65hvd230(CanTransceiver): - @override - def contents(self) -> None: - super().contents() - self.ic = self.Block(Sn65hvd230_Device()) - self.connect(self.ic.controller, self.controller) - self.connect(self.ic.can, self.can) - self.connect(self.ic.gnd, self.gnd) - self.connect(self.ic.vcc, self.pwr) - - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + @override + def contents(self) -> None: + super().contents() + self.ic = self.Block(Sn65hvd230_Device()) + self.connect(self.ic.controller, self.controller) + self.connect(self.ic.can, self.can) + self.connect(self.ic.gnd, self.gnd) + self.connect(self.ic.vcc, self.pwr) + + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) diff --git a/edg/parts/CeramicResonator_Cstne.py b/edg/parts/CeramicResonator_Cstne.py index 0974086ee..52964205f 100644 --- a/edg/parts/CeramicResonator_Cstne.py +++ b/edg/parts/CeramicResonator_Cstne.py @@ -15,18 +15,42 @@ def contents(self) -> None: def generate(self) -> None: super().generate() parts = [ # tolerance is total stackup: initial temperature, aging - (Range.from_tolerance(8e6, 0.0007 + 0.0011 + 0.0007), 'CSTNE8M00GH5L000R0', 'C882602', - 'https://www.murata.com/en/products/productdata/8801161805854/SPEC-CSTNE8M00GH5L000R0.pdf'), - (Range.from_tolerance(8e6, 0.0007 + 0.0013 + 0.0007), 'CSTNE8M00GH5C000R0', 'C341525', - 'https://www.murata.com/en/products/productdata/8801161773086/SPEC-CSTNE8M00GH5C000R0.pdf'), - (Range.from_tolerance(12e6, 0.0007 + 0.0011 + 0.0007), 'CSTNE12M0GH5L000R0', 'C2650803', - 'https://www.murata.com/en/products/productdata/8801162133534/SPEC-CSTNE12M0GH5L000R0.pdf'), - (Range.from_tolerance(12e6, 0.0007 + 0.0013 + 0.0007), 'CSTNE12M0GH5C000R0', 'C2659460', - 'https://www.murata.com/en/products/productdata/8801162100766/SPEC-CSTNE12M0GH5C000R0.pdf'), - (Range.from_tolerance(16e6, 0.0007 + 0.0013 + 0.0007), 'CSTNE16M0VH3C000R0', 'C882605', - 'https://www.murata.com/en/products/productdata/8801162264606/SPEC-CSTNE16M0VH3C000R0.pdf'), - (Range.from_tolerance(20e6, 0.0007 + 0.0011 + 0.0007), 'CSTNE20M0VH3L000R0', 'C2650766', - 'https://www.murata.com/en/products/productdata/8801161576478/SPEC-CSTNE20M0VH3L000R0.pdf'), + ( + Range.from_tolerance(8e6, 0.0007 + 0.0011 + 0.0007), + "CSTNE8M00GH5L000R0", + "C882602", + "https://www.murata.com/en/products/productdata/8801161805854/SPEC-CSTNE8M00GH5L000R0.pdf", + ), + ( + Range.from_tolerance(8e6, 0.0007 + 0.0013 + 0.0007), + "CSTNE8M00GH5C000R0", + "C341525", + "https://www.murata.com/en/products/productdata/8801161773086/SPEC-CSTNE8M00GH5C000R0.pdf", + ), + ( + Range.from_tolerance(12e6, 0.0007 + 0.0011 + 0.0007), + "CSTNE12M0GH5L000R0", + "C2650803", + "https://www.murata.com/en/products/productdata/8801162133534/SPEC-CSTNE12M0GH5L000R0.pdf", + ), + ( + Range.from_tolerance(12e6, 0.0007 + 0.0013 + 0.0007), + "CSTNE12M0GH5C000R0", + "C2659460", + "https://www.murata.com/en/products/productdata/8801162100766/SPEC-CSTNE12M0GH5C000R0.pdf", + ), + ( + Range.from_tolerance(16e6, 0.0007 + 0.0013 + 0.0007), + "CSTNE16M0VH3C000R0", + "C882605", + "https://www.murata.com/en/products/productdata/8801162264606/SPEC-CSTNE16M0VH3C000R0.pdf", + ), + ( + Range.from_tolerance(20e6, 0.0007 + 0.0011 + 0.0007), + "CSTNE20M0VH3L000R0", + "C2650766", + "https://www.murata.com/en/products/productdata/8801161576478/SPEC-CSTNE20M0VH3L000R0.pdf", + ), ] suitable_parts = [part for part in parts if part[0].fuzzy_in(self.get(self.frequency))] assert suitable_parts, "no compatible part" @@ -36,12 +60,14 @@ def generate(self) -> None: self.assign(self.lcsc_part, lcsc_part) self.assign(self.actual_basic_part, False) self.footprint( - 'U', 'Crystal:Resonator_SMD_Murata_CSTxExxV-3Pin_3.0x1.1mm', + "U", + "Crystal:Resonator_SMD_Murata_CSTxExxV-3Pin_3.0x1.1mm", { - '1': self.crystal.xtal_in, - '2': self.gnd, - '3': self.crystal.xtal_out, + "1": self.crystal.xtal_in, + "2": self.gnd, + "3": self.crystal.xtal_out, }, - mfr='Murata Electronics', part=part_number, + mfr="Murata Electronics", + part=part_number, datasheet=part_datasheet, ) diff --git a/edg/parts/Comparator_Lmv331.py b/edg/parts/Comparator_Lmv331.py index e589f190e..65e1f6ae2 100644 --- a/edg/parts/Comparator_Lmv331.py +++ b/edg/parts/Comparator_Lmv331.py @@ -8,15 +8,15 @@ class Lmv331_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vcc = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, - current_draw=(40, 100)*uAmp # Icc, typ to max - )) + self.vcc = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(40, 100) * uAmp) # Icc, typ to max + ) in_model = AnalogSink.from_supply( - self.gnd, self.vcc, + self.gnd, + self.vcc, voltage_limit_tolerance=(0, 0), - impedance=(12.5, 200)*MOhm # from input bias current @ 5v + impedance=(12.5, 200) * MOhm, # from input bias current @ 5v ) self.inn = self.Port(in_model) self.inp = self.Port(in_model) @@ -27,22 +27,25 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() self.footprint( - 'U','Package_TO_SOT_SMD:SOT-353_SC-70-5', + "U", + "Package_TO_SOT_SMD:SOT-353_SC-70-5", { - '1': self.inp, - '2': self.gnd, - '3': self.inn, - '4': self.out, - '5': self.vcc, + "1": self.inp, + "2": self.gnd, + "3": self.inn, + "4": self.out, + "5": self.vcc, }, - mfr='Texas Instruments', part='LMV331IDCKR', - datasheet='https://www.ti.com/lit/ds/symlink/lmv331.pdf' + mfr="Texas Instruments", + part="LMV331IDCKR", + datasheet="https://www.ti.com/lit/ds/symlink/lmv331.pdf", ) - self.assign(self.lcsc_part, 'C7976') + self.assign(self.lcsc_part, "C7976") class Lmv331(Comparator): """General purpose comparator""" + @override def contents(self) -> None: super().contents() @@ -53,4 +56,4 @@ def contents(self) -> None: self.connect(self.ic.inp, self.inp) self.connect(self.ic.out, self.out) - self.vdd_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/Connector_Banana.py b/edg/parts/Connector_Banana.py index 58341228e..c1196e02c 100644 --- a/edg/parts/Connector_Banana.py +++ b/edg/parts/Connector_Banana.py @@ -4,38 +4,28 @@ class Ct3151(BananaSafetyJack, FootprintBlock): - """CT3151-x PTH right-angle safety banana jack connector. - x indicates the color code. - - TODO: automatically support color code generation? - """ - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'J', 'Connector:CalTest_CT3151', - { - '1': self.port - }, - mfr='CalTest', part='CT3151-*' - ) + """CT3151-x PTH right-angle safety banana jack connector. + x indicates the color code. + + TODO: automatically support color code generation? + """ + + @override + def contents(self) -> None: + super().contents() + self.footprint("J", "Connector:CalTest_CT3151", {"1": self.port}, mfr="CalTest", part="CT3151-*") class Fcr7350(BananaSafetyJack, FootprintBlock): - """FCR7350x PTH right-angle safety banana jack connector. - x indicates the color code. - - Potentially footprint compatible with Pomona 73099 (~$9)? - - TODO: automatically support color code generation? - """ - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'J', 'edg:CLIFF_FCR7350', - { - '1': self.port - }, - mfr='CLIFF', part='FCR7350*' - ) + """FCR7350x PTH right-angle safety banana jack connector. + x indicates the color code. + + Potentially footprint compatible with Pomona 73099 (~$9)? + + TODO: automatically support color code generation? + """ + + @override + def contents(self) -> None: + super().contents() + self.footprint("J", "edg:CLIFF_FCR7350", {"1": self.port}, mfr="CLIFF", part="FCR7350*") diff --git a/edg/parts/Connector_Rf.py b/edg/parts/Connector_Rf.py index e43b7656b..02a01648d 100644 --- a/edg/parts/Connector_Rf.py +++ b/edg/parts/Connector_Rf.py @@ -6,34 +6,40 @@ class Bwipx_1_001e(RfConnectorTestPoint, UflConnector, JlcPart, FootprintBlock): """BAT WIRELESS IPEX connector""" + @override def contents(self) -> None: super().contents() self.footprint( - 'J', 'Connector_Coaxial:U.FL_Hirose_U.FL-R-SMT-1_Vertical', + "J", + "Connector_Coaxial:U.FL_Hirose_U.FL-R-SMT-1_Vertical", { - '1': self.sig, - '2': self.gnd, + "1": self.sig, + "2": self.gnd, }, value=self.tp_name, - mfr='BWIPX-1-001E', part='BAT WIRELESS', - datasheet='https://datasheet.lcsc.com/lcsc/2012231509_BAT-WIRELESS-BWIPX-1-001E_C496552.pdf' + mfr="BWIPX-1-001E", + part="BAT WIRELESS", + datasheet="https://datasheet.lcsc.com/lcsc/2012231509_BAT-WIRELESS-BWIPX-1-001E_C496552.pdf", ) - self.assign(self.lcsc_part, 'C496552') + self.assign(self.lcsc_part, "C496552") self.assign(self.actual_basic_part, False) class Amphenol901143(SmaFConnector, FootprintBlock): """PTH right-angle SMA-F connector""" + @override def contents(self) -> None: super().contents() self.footprint( - 'J', 'Connector_Coaxial:SMA_Amphenol_901-143_Horizontal', + "J", + "Connector_Coaxial:SMA_Amphenol_901-143_Horizontal", { - '1': self.sig, - '2': self.gnd, + "1": self.sig, + "2": self.gnd, }, - mfr='Amphenol RF', part='901-143', - datasheet='https://s3-us-west-2.amazonaws.com/catsy.582/C901-143.pdf' + mfr="Amphenol RF", + part="901-143", + datasheet="https://s3-us-west-2.amazonaws.com/catsy.582/C901-143.pdf", ) diff --git a/edg/parts/Connectors.py b/edg/parts/Connectors.py index f9ca26ab2..2c239c6e2 100644 --- a/edg/parts/Connectors.py +++ b/edg/parts/Connectors.py @@ -8,93 +8,116 @@ @abstract_block class PowerBarrelJack(Connector, PowerSource, Block): - """Barrel jack that models a configurable voltage / max current power supply.""" - def __init__(self, - voltage_out: RangeLike = RangeExpr(), - current_limits: RangeLike = RangeExpr.ALL) -> None: - super().__init__() + """Barrel jack that models a configurable voltage / max current power supply.""" - self.pwr = self.Port(VoltageSource(voltage_out=voltage_out, current_limits=current_limits)) - self.gnd = self.Port(Ground()) + def __init__(self, voltage_out: RangeLike = RangeExpr(), current_limits: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSource(voltage_out=voltage_out, current_limits=current_limits)) + self.gnd = self.Port(Ground()) class Pj_102ah(PowerBarrelJack, FootprintBlock): - """Barrel jack for 2.1mm ID and 5.5mm OD""" - @override - def contents(self) -> None: - super().contents() - self.require(self.pwr.voltage_out.within((0, 24)*Volt)) # datasheet ratings for connector - self.require(self.pwr.current_limits.within((0, 2.5)*Volt)) - self.footprint( - 'J', 'Connector_BarrelJack:BarrelJack_CUI_PJ-102AH_Horizontal', - { - '1': self.pwr, - '2': self.gnd, - # '3': # TODO optional switch - }, - mfr='CUI Devices', part='PJ-102AH', - datasheet='https://www.cui.com/product/resource/digikeypdf/pj-102a.pdf' - ) + """Barrel jack for 2.1mm ID and 5.5mm OD""" + + @override + def contents(self) -> None: + super().contents() + self.require(self.pwr.voltage_out.within((0, 24) * Volt)) # datasheet ratings for connector + self.require(self.pwr.current_limits.within((0, 2.5) * Volt)) + self.footprint( + "J", + "Connector_BarrelJack:BarrelJack_CUI_PJ-102AH_Horizontal", + { + "1": self.pwr, + "2": self.gnd, + # '3': # TODO optional switch + }, + mfr="CUI Devices", + part="PJ-102AH", + datasheet="https://www.cui.com/product/resource/digikeypdf/pj-102a.pdf", + ) class Pj_036ah(PowerBarrelJack, FootprintBlock): - """SMT Barrel jack for 2.1mm ID and 5.5mm OD""" - @override - def contents(self) -> None: - super().contents() - self.require(self.pwr.voltage_out.within((0, 24)*Volt)) # datasheet ratings for connector - self.require(self.pwr.current_limits.within((0, 5)*Volt)) - - self.footprint( - 'J', 'Connector_BarrelJack:BarrelJack_CUI_PJ-036AH-SMT_Horizontal', - { - '1': self.pwr, - '2': self.gnd, - # '3': # TODO optional switch - }, - mfr='CUI Devices', part='PJ-036AH-SMT', - datasheet='https://www.cuidevices.com/product/resource/pj-036ah-smt-tr.pdf' - ) + """SMT Barrel jack for 2.1mm ID and 5.5mm OD""" + + @override + def contents(self) -> None: + super().contents() + self.require(self.pwr.voltage_out.within((0, 24) * Volt)) # datasheet ratings for connector + self.require(self.pwr.current_limits.within((0, 5) * Volt)) + + self.footprint( + "J", + "Connector_BarrelJack:BarrelJack_CUI_PJ-036AH-SMT_Horizontal", + { + "1": self.pwr, + "2": self.gnd, + # '3': # TODO optional switch + }, + mfr="CUI Devices", + part="PJ-036AH-SMT", + datasheet="https://www.cuidevices.com/product/resource/pj-036ah-smt-tr.pdf", + ) class LipoConnector(Connector, Battery): - """PassiveConnector (abstract connector) that is expected to have a LiPo on one end. - Both the voltage specification and the actual voltage can be specified as parameters. - THERE IS NO STANDARD LIPO PINNING OR CONNECTOR - MAKE SURE TO VERIFY THIS! - BE PREPARED FOR REVERSE POLARITY CONNECTIONS. - Default pinning has ground being pin 1, and power being pin 2. - - Connector type not specified, up to the user through a refinement.""" - def __init__(self, voltage: RangeLike = (2.5, 4.2)*Volt, *args: Any, - actual_voltage: RangeLike = (2.5, 4.2)*Volt, **kwargs: Any) -> None: - from ..electronics_model.PassivePort import PassiveAdapterVoltageSink - super().__init__(voltage, *args, **kwargs) - self.chg = self.Port(VoltageSink.empty(), optional=True) # ideal port for charging - self.conn = self.Block(PassiveConnector()) - - self.connect(self.gnd, self.conn.pins.request('1').adapt_to(Ground())) - pwr_pin = self.conn.pins.request('2') - self.connect(self.pwr, pwr_pin.adapt_to(VoltageSource( - voltage_out=actual_voltage, # arbitrary from https://www.mouser.com/catalog/additional/Adafruit_3262.pdf - current_limits=(0, 5.5)*Amp, # arbitrary assuming low capacity, 10 C discharge - ))) - self.chg_adapter = self.Block(PassiveAdapterVoltageSink()) - self.connect(pwr_pin, self.chg_adapter.src) - self.connect(self.chg, self.chg_adapter.dst) - self.assign(self.actual_capacity, (500, 600)*mAmp) # arbitrary + """PassiveConnector (abstract connector) that is expected to have a LiPo on one end. + Both the voltage specification and the actual voltage can be specified as parameters. + THERE IS NO STANDARD LIPO PINNING OR CONNECTOR - MAKE SURE TO VERIFY THIS! + BE PREPARED FOR REVERSE POLARITY CONNECTIONS. + Default pinning has ground being pin 1, and power being pin 2. + + Connector type not specified, up to the user through a refinement.""" + + def __init__( + self, + voltage: RangeLike = (2.5, 4.2) * Volt, + *args: Any, + actual_voltage: RangeLike = (2.5, 4.2) * Volt, + **kwargs: Any, + ) -> None: + from ..electronics_model.PassivePort import PassiveAdapterVoltageSink + + super().__init__(voltage, *args, **kwargs) + self.chg = self.Port(VoltageSink.empty(), optional=True) # ideal port for charging + self.conn = self.Block(PassiveConnector()) + + self.connect(self.gnd, self.conn.pins.request("1").adapt_to(Ground())) + pwr_pin = self.conn.pins.request("2") + self.connect( + self.pwr, + pwr_pin.adapt_to( + VoltageSource( + voltage_out=actual_voltage, # arbitrary from https://www.mouser.com/catalog/additional/Adafruit_3262.pdf + current_limits=(0, 5.5) * Amp, # arbitrary assuming low capacity, 10 C discharge + ) + ), + ) + self.chg_adapter = self.Block(PassiveAdapterVoltageSink()) + self.connect(pwr_pin, self.chg_adapter.src) + self.connect(self.chg, self.chg_adapter.dst) + self.assign(self.actual_capacity, (500, 600) * mAmp) # arbitrary class QwiicTarget(Connector): - """A Qwiic (https://www.sparkfun.com/qwiic) connector to a I2C target. - This would be on a board with a host controller.""" - def __init__(self) -> None: - super().__init__() - self.conn = self.Block(JstShSmHorizontal(4)) - self.gnd = self.Export(self.conn.pins.request('1').adapt_to(Ground()), [Common]) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSink( - voltage_limits=3.3*Volt(tol=0.05), # required 3.3v per the spec, tolerance assumed - current_draw=(0, 226)*mAmp, # per the Qwiic FAQ, max current for the cable - )), [Power]) - self.i2c = self.Port(I2cTarget(DigitalBidir.empty()), [InOut]) - self.connect(self.i2c.sda, self.conn.pins.request('3').adapt_to(DigitalBidir())) - self.connect(self.i2c.scl, self.conn.pins.request('4').adapt_to(DigitalBidir())) + """A Qwiic (https://www.sparkfun.com/qwiic) connector to a I2C target. + This would be on a board with a host controller.""" + + def __init__(self) -> None: + super().__init__() + self.conn = self.Block(JstShSmHorizontal(4)) + self.gnd = self.Export(self.conn.pins.request("1").adapt_to(Ground()), [Common]) + self.pwr = self.Export( + self.conn.pins.request("2").adapt_to( + VoltageSink( + voltage_limits=3.3 * Volt(tol=0.05), # required 3.3v per the spec, tolerance assumed + current_draw=(0, 226) * mAmp, # per the Qwiic FAQ, max current for the cable + ) + ), + [Power], + ) + self.i2c = self.Port(I2cTarget(DigitalBidir.empty()), [InOut]) + self.connect(self.i2c.sda, self.conn.pins.request("3").adapt_to(DigitalBidir())) + self.connect(self.i2c.scl, self.conn.pins.request("4").adapt_to(DigitalBidir())) diff --git a/edg/parts/CurrentSense_Ad8418.py b/edg/parts/CurrentSense_Ad8418.py index 80cbeabe5..aeffc7563 100644 --- a/edg/parts/CurrentSense_Ad8418.py +++ b/edg/parts/CurrentSense_Ad8418.py @@ -8,17 +8,17 @@ class Ad8418a_Device(JlcPart, FootprintBlock, InternalSubcircuit): GAIN = Range.from_tolerance(20, 0.0015) + def __init__(self, in_diff_range: RangeLike): super().__init__() self.gnd = self.Port(Ground(), [Common]) - self.vs = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, - current_draw=(4.1, 4.2)*mAmp # maximum only - )) + self.vs = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(4.1, 4.2) * mAmp) # maximum only + ) input_model = AnalogSink( - voltage_limits=(2, 70)*Volt, - signal_limits=(2, 70)*Volt, - impedance=(46.1, 92.3)*kOhm # at IN=12v, 130-260uA bias current + voltage_limits=(2, 70) * Volt, + signal_limits=(2, 70) * Volt, + impedance=(46.1, 92.3) * kOhm, # at IN=12v, 130-260uA bias current ) self.inn = self.Port(input_model) self.inp = self.Port(input_model) @@ -28,44 +28,43 @@ def __init__(self, in_diff_range: RangeLike): self.vref2 = self.Port(ref_model) self.in_diff_range = self.ArgParameter(in_diff_range) - self.out = self.Port(AnalogSource( - voltage_out=(0.032, self.vs.link().voltage.upper() - 0.032), - signal_out=(self.vref1.link().signal + self.vref2.link().signal) / 2 + - (self.in_diff_range * self.GAIN), - impedance=2*Ohm(tol=0) # range not specified - )) + self.out = self.Port( + AnalogSource( + voltage_out=(0.032, self.vs.link().voltage.upper() - 0.032), + signal_out=(self.vref1.link().signal + self.vref2.link().signal) / 2 + (self.in_diff_range * self.GAIN), + impedance=2 * Ohm(tol=0), # range not specified + ) + ) @override def contents(self) -> None: super().contents() self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", { - '1': self.inn, - '2': self.gnd, - '3': self.vref2, + "1": self.inn, + "2": self.gnd, + "3": self.vref2, # 4 is NC - '5': self.out, - '6': self.vs, - '7': self.vref1, - '8': self.inp, + "5": self.out, + "6": self.vs, + "7": self.vref1, + "8": self.inp, }, - mfr='Analog Devices', part='AD8418AWBRZ', - datasheet='https://www.analog.com/media/en/technical-documentation/data-sheets/ad8418.pdf' + mfr="Analog Devices", + part="AD8418AWBRZ", + datasheet="https://www.analog.com/media/en/technical-documentation/data-sheets/ad8418.pdf", ) - self.assign(self.lcsc_part, 'C462197') + self.assign(self.lcsc_part, "C462197") self.assign(self.actual_basic_part, False) class Ad8418a(CurrentSensor, KiCadImportableBlock, Block): @override def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'edg_importable:DifferentialAmplifier' - return { - '+': self.sense_pos, '-': self.sense_neg, - 'R': self.ref, '3': self.out, - 'V+': self.pwr, 'V-': self.gnd - } + assert symbol_name == "edg_importable:DifferentialAmplifier" + return {"+": self.sense_pos, "-": self.sense_neg, "R": self.ref, "3": self.out, "V+": self.pwr, "V-": self.gnd} def __init__(self, in_diff_range: RangeLike): super().__init__() @@ -83,6 +82,8 @@ def __init__(self, in_diff_range: RangeLike): def contents(self) -> None: super().contents() self.connect(self.ref, self.amp.vref2) - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) diff --git a/edg/parts/DacI2c_Mcp4728.py b/edg/parts/DacI2c_Mcp4728.py index f61efe9fb..21fa92956 100644 --- a/edg/parts/DacI2c_Mcp4728.py +++ b/edg/parts/DacI2c_Mcp4728.py @@ -5,100 +5,110 @@ class Mcp4728_Device(InternalSubcircuit, FootprintBlock, GeneratorBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.vss = self.Port(Ground()) - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, - current_draw=(0.040, 1400)*uAmp)) # power down to all channels in normal mode - - out_model = AnalogSource.from_supply( - self.vss, self.vdd, # assumed Vref=Vdd, also configurable - current_limits=(-15, 15)*mAmp, # short circuit current, typ - impedance=(1, 1)*Ohm # DC output impednace in normal mode - ) - self.vout0 = self.Port(out_model, optional=True) - self.vout1 = self.Port(out_model, optional=True) - self.vout2 = self.Port(out_model, optional=True) - self.vout3 = self.Port(out_model, optional=True) - - dio_model = DigitalBidir.from_supply( - self.vss, self.vdd, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - input_threshold_factor=(0.3, 0.7) # for Vdd >= 2.7v - ) - self.i2c = self.Port(I2cTarget(dio_model, addresses=[0x60])) # TODO 3LSBs EEPROM programmable - self.ldac = self.Port(DigitalSink.from_bidir(dio_model), optional=True) - self.rdy = self.Port(DigitalSource.low_from_supply(self.vss), optional=True) - - self.generator_param(self.ldac.is_connected()) - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.ldac.is_connected()): - ldac_pin: CircuitPort = self.ldac - else: - ldac_pin = self.vdd - - self.footprint( - 'U', 'Package_SO:MSOP-10_3x3mm_P0.5mm', - { - '1': self.vdd, - '2': self.i2c.scl, - '3': self.i2c.sda, - '4': ldac_pin, - '5': self.rdy, # float if not connected - '6': self.vout0, - '7': self.vout1, - '8': self.vout2, - '9': self.vout3, - '10': self.vss, - }, - mfr='Microchip Technology', part='MCP4728T-E/UN', - datasheet='https://ww1.microchip.com/downloads/aemDocuments/documents/OTH/ProductDocuments/DataSheets/22187E.pdf' - ) - self.assign(self.lcsc_part, 'C478093') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + self.vss = self.Port(Ground()) + self.vdd = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(0.040, 1400) * uAmp) + ) # power down to all channels in normal mode + + out_model = AnalogSource.from_supply( + self.vss, + self.vdd, # assumed Vref=Vdd, also configurable + current_limits=(-15, 15) * mAmp, # short circuit current, typ + impedance=(1, 1) * Ohm, # DC output impednace in normal mode + ) + self.vout0 = self.Port(out_model, optional=True) + self.vout1 = self.Port(out_model, optional=True) + self.vout2 = self.Port(out_model, optional=True) + self.vout3 = self.Port(out_model, optional=True) + + dio_model = DigitalBidir.from_supply( + self.vss, + self.vdd, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + input_threshold_factor=(0.3, 0.7), # for Vdd >= 2.7v + ) + self.i2c = self.Port(I2cTarget(dio_model, addresses=[0x60])) # TODO 3LSBs EEPROM programmable + self.ldac = self.Port(DigitalSink.from_bidir(dio_model), optional=True) + self.rdy = self.Port(DigitalSource.low_from_supply(self.vss), optional=True) + + self.generator_param(self.ldac.is_connected()) + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.ldac.is_connected()): + ldac_pin: CircuitPort = self.ldac + else: + ldac_pin = self.vdd + + self.footprint( + "U", + "Package_SO:MSOP-10_3x3mm_P0.5mm", + { + "1": self.vdd, + "2": self.i2c.scl, + "3": self.i2c.sda, + "4": ldac_pin, + "5": self.rdy, # float if not connected + "6": self.vout0, + "7": self.vout1, + "8": self.vout2, + "9": self.vout3, + "10": self.vss, + }, + mfr="Microchip Technology", + part="MCP4728T-E/UN", + datasheet="https://ww1.microchip.com/downloads/aemDocuments/documents/OTH/ProductDocuments/DataSheets/22187E.pdf", + ) + self.assign(self.lcsc_part, "C478093") + self.assign(self.actual_basic_part, False) class Mcp4728(DigitalToAnalog, GeneratorBlock): - """MCP4728 quad 12-bit I2C DAC, with selectable internal or external Vref=Vdd. - Note, MCP47F seems to be a similar architecture but the example application has an optional - 0.1uF capacitor on the VoutN lines to reduce noise, which is generated by default here. - """ - def __init__(self, output_caps: BoolLike = True) -> None: - super().__init__() - self.ic = self.Block(Mcp4728_Device()) - self.pwr = self.Export(self.ic.vdd, [Power]) - self.gnd = self.Export(self.ic.vss, [Common]) - - self.out0 = self.Export(self.ic.vout0, optional=True) - self.out1 = self.Export(self.ic.vout1, optional=True) - self.out2 = self.Export(self.ic.vout2, optional=True) - self.out3 = self.Export(self.ic.vout3, optional=True) - - self.i2c = self.Export(self.ic.i2c) - self.ldac = self.Export(self.ic.ldac, optional=True) # can update per-channel by i2c - self.rdy = self.Export(self.ic.rdy, optional=True) # can be read from i2c - - self.output_caps = self.ArgParameter(output_caps) - self.generator_param(self.output_caps, self.out0.is_connected(), self.out1.is_connected(), - self.out2.is_connected(), self.out3.is_connected()) - - @override - def generate(self) -> None: - super().generate() - - self.vdd_cap = ElementDict[DecouplingCapacitor]() - self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.vdd_cap[1] = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - - if self.get(self.output_caps): - self.out_cap = ElementDict[Capacitor]() - for i, out_port in [(0, self.out0), (1, self.out1), (2, self.out2), (3, self.out3)]: - if self.get(out_port.is_connected()): - self.out_cap[i] = out_cap = self.Block(Capacitor(0.1*uFarad(tol=0.2), out_port.link().voltage)) - self.connect(out_cap.pos.adapt_to(AnalogSink()), out_port) - self.connect(out_cap.neg.adapt_to(Ground()), self.gnd) + """MCP4728 quad 12-bit I2C DAC, with selectable internal or external Vref=Vdd. + Note, MCP47F seems to be a similar architecture but the example application has an optional + 0.1uF capacitor on the VoutN lines to reduce noise, which is generated by default here. + """ + + def __init__(self, output_caps: BoolLike = True) -> None: + super().__init__() + self.ic = self.Block(Mcp4728_Device()) + self.pwr = self.Export(self.ic.vdd, [Power]) + self.gnd = self.Export(self.ic.vss, [Common]) + + self.out0 = self.Export(self.ic.vout0, optional=True) + self.out1 = self.Export(self.ic.vout1, optional=True) + self.out2 = self.Export(self.ic.vout2, optional=True) + self.out3 = self.Export(self.ic.vout3, optional=True) + + self.i2c = self.Export(self.ic.i2c) + self.ldac = self.Export(self.ic.ldac, optional=True) # can update per-channel by i2c + self.rdy = self.Export(self.ic.rdy, optional=True) # can be read from i2c + + self.output_caps = self.ArgParameter(output_caps) + self.generator_param( + self.output_caps, + self.out0.is_connected(), + self.out1.is_connected(), + self.out2.is_connected(), + self.out3.is_connected(), + ) + + @override + def generate(self) -> None: + super().generate() + + self.vdd_cap = ElementDict[DecouplingCapacitor]() + self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap[1] = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + + if self.get(self.output_caps): + self.out_cap = ElementDict[Capacitor]() + for i, out_port in [(0, self.out0), (1, self.out1), (2, self.out2), (3, self.out3)]: + if self.get(out_port.is_connected()): + self.out_cap[i] = out_cap = self.Block(Capacitor(0.1 * uFarad(tol=0.2), out_port.link().voltage)) + self.connect(out_cap.pos.adapt_to(AnalogSink()), out_port) + self.connect(out_cap.neg.adapt_to(Ground()), self.gnd) diff --git a/edg/parts/DacI2c_Mcp47f.py b/edg/parts/DacI2c_Mcp47f.py index dde950528..5356264a8 100644 --- a/edg/parts/DacI2c_Mcp47f.py +++ b/edg/parts/DacI2c_Mcp47f.py @@ -4,109 +4,114 @@ class Mcp47f_Device(InternalSubcircuit, FootprintBlock, GeneratorBlock): - def __init__(self, addr_lsb: IntLike) -> None: - super().__init__() - self.vss = self.Port(Ground()) - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, # technically down to 1.8 w/ reduced performance - current_draw=(0.00085, 2.5)*mAmp)) # quad DAC, serial inactive to EE write - - ref_model = VoltageSink( - voltage_limits=self.vss.link().voltage.hull(self.vdd.link().voltage) - ) - self.vref0 = self.Port(ref_model) - self.vref1 = self.Port(ref_model) - - out_ref0_model = AnalogSource.from_supply( - self.vss, self.vref0, - signal_out_bound=(0.01*Volt, -0.016*Volt), # output amp min / max voltages - current_limits=(-3, 3)*mAmp, # short circuit current, typ - impedance=(122, 900)*Ohm # derived from assumed Vout=Vdd=2.7v, Isc=3-22mA - ) - out_ref1_model = AnalogSource.from_supply( - self.vss, self.vref1, - signal_out_bound=(0.01*Volt, -0.016*Volt), # output amp min / max voltages - current_limits=(-3, 3)*mAmp, # short circuit current, typ - impedance=(122, 900)*Ohm # derived from assumed Vout=Vdd=2.7v, Isc=3-22mA - ) - self.vout0 = self.Port(out_ref0_model, optional=True) - self.vout1 = self.Port(out_ref1_model, optional=True) - self.vout2 = self.Port(out_ref0_model, optional=True) - self.vout3 = self.Port(out_ref1_model, optional=True) - - dio_model = DigitalBidir.from_supply( # LAT0/1/HVC, same input thresholds for I2C - self.vss, self.vdd, - voltage_limit_tolerance=(-0.6, 0.3)*Volt, - current_limits=(-2, 2)*mAmp, - input_threshold_factor=(0.3, 0.7) - ) - self.lat0 = self.Port(dio_model) - self.lat1 = self.Port(dio_model) - self.i2c = self.Port(I2cTarget(dio_model, addresses=ArrayIntExpr())) - - self.addr_lsb = self.ArgParameter(addr_lsb) - self.generator_param(self.addr_lsb) - - @override - def generate(self) -> None: - super().generate() - - addr_lsb = self.get(self.addr_lsb) - self.require((addr_lsb < 4) & (addr_lsb >= 0), f"addr_lsb={addr_lsb} must be within [0, 4)") - self.assign(self.i2c.addresses, [0x60 | addr_lsb]) # fixed for volatile devices - - self.footprint( - 'U', 'Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm', - { - '1': self.lat1, - '2': self.vdd, - '3': self.vdd if addr_lsb & 1 else self.vss, # A0 - '4': self.vref0, - '5': self.vout0, - '6': self.vout2, - # '7', '8': nc - '9': self.vss, - # '10', '11', '12', '13': nc - '14': self.vout3, - '15': self.vout1, - '16': self.vref1, - '17': self.vdd if addr_lsb & 2 else self.vss, # A1 - '18': self.i2c.scl, - '19': self.i2c.sda, - '20': self.lat0 - - }, - mfr='Microchip Technology', part='MCP47FVB24T-20E/ST', - datasheet='https://ww1.microchip.com/downloads/aemDocuments/documents/MSLD/ProductDocuments/DataSheets/MCP47FXBX48-Data-Sheet-DS200006368A.pdf' - ) + def __init__(self, addr_lsb: IntLike) -> None: + super().__init__() + self.vss = self.Port(Ground()) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(2.7, 5.5) * Volt, # technically down to 1.8 w/ reduced performance + current_draw=(0.00085, 2.5) * mAmp, + ) + ) # quad DAC, serial inactive to EE write + + ref_model = VoltageSink(voltage_limits=self.vss.link().voltage.hull(self.vdd.link().voltage)) + self.vref0 = self.Port(ref_model) + self.vref1 = self.Port(ref_model) + + out_ref0_model = AnalogSource.from_supply( + self.vss, + self.vref0, + signal_out_bound=(0.01 * Volt, -0.016 * Volt), # output amp min / max voltages + current_limits=(-3, 3) * mAmp, # short circuit current, typ + impedance=(122, 900) * Ohm, # derived from assumed Vout=Vdd=2.7v, Isc=3-22mA + ) + out_ref1_model = AnalogSource.from_supply( + self.vss, + self.vref1, + signal_out_bound=(0.01 * Volt, -0.016 * Volt), # output amp min / max voltages + current_limits=(-3, 3) * mAmp, # short circuit current, typ + impedance=(122, 900) * Ohm, # derived from assumed Vout=Vdd=2.7v, Isc=3-22mA + ) + self.vout0 = self.Port(out_ref0_model, optional=True) + self.vout1 = self.Port(out_ref1_model, optional=True) + self.vout2 = self.Port(out_ref0_model, optional=True) + self.vout3 = self.Port(out_ref1_model, optional=True) + + dio_model = DigitalBidir.from_supply( # LAT0/1/HVC, same input thresholds for I2C + self.vss, + self.vdd, + voltage_limit_tolerance=(-0.6, 0.3) * Volt, + current_limits=(-2, 2) * mAmp, + input_threshold_factor=(0.3, 0.7), + ) + self.lat0 = self.Port(dio_model) + self.lat1 = self.Port(dio_model) + self.i2c = self.Port(I2cTarget(dio_model, addresses=ArrayIntExpr())) + + self.addr_lsb = self.ArgParameter(addr_lsb) + self.generator_param(self.addr_lsb) + + @override + def generate(self) -> None: + super().generate() + + addr_lsb = self.get(self.addr_lsb) + self.require((addr_lsb < 4) & (addr_lsb >= 0), f"addr_lsb={addr_lsb} must be within [0, 4)") + self.assign(self.i2c.addresses, [0x60 | addr_lsb]) # fixed for volatile devices + + self.footprint( + "U", + "Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm", + { + "1": self.lat1, + "2": self.vdd, + "3": self.vdd if addr_lsb & 1 else self.vss, # A0 + "4": self.vref0, + "5": self.vout0, + "6": self.vout2, + # '7', '8': nc + "9": self.vss, + # '10', '11', '12', '13': nc + "14": self.vout3, + "15": self.vout1, + "16": self.vref1, + "17": self.vdd if addr_lsb & 2 else self.vss, # A1 + "18": self.i2c.scl, + "19": self.i2c.sda, + "20": self.lat0, + }, + mfr="Microchip Technology", + part="MCP47FVB24T-20E/ST", + datasheet="https://ww1.microchip.com/downloads/aemDocuments/documents/MSLD/ProductDocuments/DataSheets/MCP47FXBX48-Data-Sheet-DS200006368A.pdf", + ) class Mcp47f(DigitalToAnalog, Block): - """MCP47FxBx4/8 quad / octal 8/10/12-bit I2C DAC, with selectable internal or external Vref - """ - def __init__(self, addr_lsb: IntLike = 0) -> None: - super().__init__() - self.ic = self.Block(Mcp47f_Device(addr_lsb=addr_lsb)) - self.pwr = self.Export(self.ic.vdd, [Power]) - self.gnd = self.Export(self.ic.vss, [Common]) - - self.ref0 = self.Export(self.ic.vref0) - self.ref1 = self.Export(self.ic.vref1) - - self.out0 = self.Export(self.ic.vout0, optional=True) - self.out1 = self.Export(self.ic.vout1, optional=True) - self.out2 = self.Export(self.ic.vout2, optional=True) - self.out3 = self.Export(self.ic.vout3, optional=True) - - self.i2c = self.Export(self.ic.i2c) - self.lat0 = self.Export(self.ic.lat0) - self.lat1 = self.Export(self.ic.lat1) - - @override - def contents(self) -> None: - super().contents() - - # Datasheet section 6.2, example uses two bypass capacitors - self.vdd_cap = ElementDict[DecouplingCapacitor]() - self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.vdd_cap[1] = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + """MCP47FxBx4/8 quad / octal 8/10/12-bit I2C DAC, with selectable internal or external Vref""" + + def __init__(self, addr_lsb: IntLike = 0) -> None: + super().__init__() + self.ic = self.Block(Mcp47f_Device(addr_lsb=addr_lsb)) + self.pwr = self.Export(self.ic.vdd, [Power]) + self.gnd = self.Export(self.ic.vss, [Common]) + + self.ref0 = self.Export(self.ic.vref0) + self.ref1 = self.Export(self.ic.vref1) + + self.out0 = self.Export(self.ic.vout0, optional=True) + self.out1 = self.Export(self.ic.vout1, optional=True) + self.out2 = self.Export(self.ic.vout2, optional=True) + self.out3 = self.Export(self.ic.vout3, optional=True) + + self.i2c = self.Export(self.ic.i2c) + self.lat0 = self.Export(self.ic.lat0) + self.lat1 = self.Export(self.ic.lat1) + + @override + def contents(self) -> None: + super().contents() + + # Datasheet section 6.2, example uses two bypass capacitors + self.vdd_cap = ElementDict[DecouplingCapacitor]() + self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap[1] = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/DacSpi_Mcp4901.py b/edg/parts/DacSpi_Mcp4901.py index 150b65c5b..98bf8dfeb 100644 --- a/edg/parts/DacSpi_Mcp4901.py +++ b/edg/parts/DacSpi_Mcp4901.py @@ -4,75 +4,84 @@ class Mcp4921_Device(InternalSubcircuit, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, - current_draw=(3.3, 350)*uAmp)) # from software shutdown to operating - self.vss = self.Port(Ground()) + def __init__(self) -> None: + super().__init__() + self.vdd = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(3.3, 350) * uAmp) + ) # from software shutdown to operating + self.vss = self.Port(Ground()) - self.vref = self.Port(VoltageSink( - voltage_limits=(0.04*Volt, self.vdd.link().voltage.lower() - 0.04), - )) - self.vout = self.Port(AnalogSource.from_supply( - self.vss, self.vref, - signal_out_bound=(0.01*Volt, -0.04*Volt), # output swing - current_limits=(-15, 15)*mAmp, # short circuit current, typ - impedance=(171, 273)*Ohm # derived from assumed Vout=2Vref=4.096, Isc=24mA or 15mA - )) + self.vref = self.Port( + VoltageSink( + voltage_limits=(0.04 * Volt, self.vdd.link().voltage.lower() - 0.04), + ) + ) + self.vout = self.Port( + AnalogSource.from_supply( + self.vss, + self.vref, + signal_out_bound=(0.01 * Volt, -0.04 * Volt), # output swing + current_limits=(-15, 15) * mAmp, # short circuit current, typ + impedance=(171, 273) * Ohm, # derived from assumed Vout=2Vref=4.096, Isc=24mA or 15mA + ) + ) - dio_model = DigitalBidir.from_supply( - self.vss, self.vdd, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - current_limits=(-25, 25)*mAmp, - input_threshold_factor=(0.2, 0.7) - ) - self.ldac = self.Port(dio_model) - self.spi = self.Port(SpiPeripheral(dio_model, frequency_limit=(0, 20) * MHertz)) - self.cs = self.Port(dio_model) + dio_model = DigitalBidir.from_supply( + self.vss, + self.vdd, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_limits=(-25, 25) * mAmp, + input_threshold_factor=(0.2, 0.7), + ) + self.ldac = self.Port(dio_model) + self.spi = self.Port(SpiPeripheral(dio_model, frequency_limit=(0, 20) * MHertz)) + self.cs = self.Port(dio_model) - @override - def contents(self) -> None: - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.vdd, - '2': self.cs, - '3': self.spi.sck, - '4': self.spi.mosi, - '5': self.ldac, - '6': self.vref, - '7': self.vss, - '8': self.vout, - }, - mfr='Microchip Technology', part='MCP4921-E/SN', - datasheet='https://ww1.microchip.com/downloads/en/DeviceDoc/22248a.pdf' - ) + @override + def contents(self) -> None: + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.vdd, + "2": self.cs, + "3": self.spi.sck, + "4": self.spi.mosi, + "5": self.ldac, + "6": self.vref, + "7": self.vss, + "8": self.vout, + }, + mfr="Microchip Technology", + part="MCP4921-E/SN", + datasheet="https://ww1.microchip.com/downloads/en/DeviceDoc/22248a.pdf", + ) class Mcp4921(DigitalToAnalog, Block): - """MCP4921 12-bit 4.5uS DAC. - Other chips in series: - MCP4901 (8 bits), MCP4911 (10 bits), and others with 2 channels or internal Vref - """ - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Mcp4921_Device()) - self.pwr = self.Export(self.ic.vdd, [Power]) - self.gnd = self.Export(self.ic.vss, [Common]) + """MCP4921 12-bit 4.5uS DAC. + Other chips in series: + MCP4901 (8 bits), MCP4911 (10 bits), and others with 2 channels or internal Vref + """ - self.ref = self.Export(self.ic.vref) - self.out = self.Export(self.ic.vout, [Output]) + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Mcp4921_Device()) + self.pwr = self.Export(self.ic.vdd, [Power]) + self.gnd = self.Export(self.ic.vss, [Common]) - self.spi = self.Export(self.ic.spi, [Input]) - self.cs = self.Export(self.ic.cs) - self.ldac = self.Export(self.ic.ldac) + self.ref = self.Export(self.ic.vref) + self.out = self.Export(self.ic.vout, [Output]) - @override - def contents(self) -> None: - super().contents() + self.spi = self.Export(self.ic.spi, [Input]) + self.cs = self.Export(self.ic.cs) + self.ldac = self.Export(self.ic.ldac) - # Datasheet section 6.2, example uses two bypass capacitors - self.vdd_cap = ElementDict[DecouplingCapacitor]() - self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.vdd_cap[1] = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + @override + def contents(self) -> None: + super().contents() + + # Datasheet section 6.2, example uses two bypass capacitors + self.vdd_cap = ElementDict[DecouplingCapacitor]() + self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap[1] = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/DebugHeaders.py b/edg/parts/DebugHeaders.py index 887b7f76e..fc776c1c5 100644 --- a/edg/parts/DebugHeaders.py +++ b/edg/parts/DebugHeaders.py @@ -5,54 +5,64 @@ from .PassiveConnector_TagConnect import TagConnect -class SwdCortexTargetHeader(SwdCortexTargetConnector, SwdCortexTargetConnectorReset, SwdCortexTargetConnectorSwo, - SwdCortexTargetConnectorTdi): - @override - def contents(self) -> None: - super().contents() - self.conn = self.Block(PinHeader127DualShrouded(10)) - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - self.connect(self.gnd, self.conn.pins.request('3').adapt_to(Ground()), - self.conn.pins.request('5').adapt_to(Ground()), - self.conn.pins.request('9').adapt_to(Ground())) - self.connect(self.swd.swdio, self.conn.pins.request('2').adapt_to(DigitalBidir())) - self.connect(self.swd.swclk, self.conn.pins.request('4').adapt_to(DigitalSource())) - self.connect(self.swo, self.conn.pins.request('6').adapt_to(DigitalBidir())) - self.connect(self.tdi, self.conn.pins.request('8').adapt_to(DigitalBidir())) - # TODO: pulldown is a hack to prevent driver conflict warnings, this should be a active low (open drain) driver - self.connect(self.reset, self.conn.pins.request('10').adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) +class SwdCortexTargetHeader( + SwdCortexTargetConnector, SwdCortexTargetConnectorReset, SwdCortexTargetConnectorSwo, SwdCortexTargetConnectorTdi +): + @override + def contents(self) -> None: + super().contents() + self.conn = self.Block(PinHeader127DualShrouded(10)) + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + self.connect( + self.gnd, + self.conn.pins.request("3").adapt_to(Ground()), + self.conn.pins.request("5").adapt_to(Ground()), + self.conn.pins.request("9").adapt_to(Ground()), + ) + self.connect(self.swd.swdio, self.conn.pins.request("2").adapt_to(DigitalBidir())) + self.connect(self.swd.swclk, self.conn.pins.request("4").adapt_to(DigitalSource())) + self.connect(self.swo, self.conn.pins.request("6").adapt_to(DigitalBidir())) + self.connect(self.tdi, self.conn.pins.request("8").adapt_to(DigitalBidir())) + # TODO: pulldown is a hack to prevent driver conflict warnings, this should be a active low (open drain) driver + self.connect(self.reset, self.conn.pins.request("10").adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) class SwdCortexTargetTagConnect(SwdCortexTargetConnector, SwdCortexTargetConnectorReset, SwdCortexTargetConnectorSwo): - """OFFICIAL tag connect SWD header using the TC2030 series cables. - https://www.tag-connect.com/wp-content/uploads/bsk-pdf-manager/TC2030-CTX_1.pdf""" - @override - def contents(self) -> None: - super().contents() - self.conn = self.Block(TagConnect(6)) - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - self.connect(self.swd.swdio, self.conn.pins.request('2').adapt_to(DigitalBidir())) # also TMS - # TODO: pulldown is a hack to prevent driver conflict warnings, this should be a active low (open drain) driver - self.connect(self.reset, self.conn.pins.request('3').adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) - self.connect(self.swd.swclk, self.conn.pins.request('4').adapt_to(DigitalSource())) - self.connect(self.gnd, self.conn.pins.request('5').adapt_to(Ground())) - self.connect(self.swo, self.conn.pins.request('6').adapt_to(DigitalBidir())) - - -class SwdCortexTargetTc2050(SwdCortexTargetConnector, SwdCortexTargetConnectorReset, SwdCortexTargetConnectorSwo, - SwdCortexTargetConnectorTdi): - """UNOFFICIAL tag connect SWD header, maintaining physical pin compatibility with the 2x05 1.27mm header.""" - @override - def contents(self) -> None: - super().contents() - self.conn = self.Block(TagConnect(10)) - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - self.connect(self.gnd, self.conn.pins.request('2').adapt_to(Ground()), - self.conn.pins.request('3').adapt_to(Ground()), - self.conn.pins.request('5').adapt_to(Ground())) - self.connect(self.swd.swdio, self.conn.pins.request('10').adapt_to(DigitalBidir())) - self.connect(self.swd.swclk, self.conn.pins.request('9').adapt_to(DigitalSource())) - self.connect(self.swo, self.conn.pins.request('8').adapt_to(DigitalBidir())) - self.connect(self.tdi, self.conn.pins.request('7').adapt_to(DigitalBidir())) - # TODO: pulldown is a hack to prevent driver conflict warnings, this should be a active low (open drain) driver - self.connect(self.reset, self.conn.pins.request('6').adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) + """OFFICIAL tag connect SWD header using the TC2030 series cables. + https://www.tag-connect.com/wp-content/uploads/bsk-pdf-manager/TC2030-CTX_1.pdf""" + + @override + def contents(self) -> None: + super().contents() + self.conn = self.Block(TagConnect(6)) + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + self.connect(self.swd.swdio, self.conn.pins.request("2").adapt_to(DigitalBidir())) # also TMS + # TODO: pulldown is a hack to prevent driver conflict warnings, this should be a active low (open drain) driver + self.connect(self.reset, self.conn.pins.request("3").adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) + self.connect(self.swd.swclk, self.conn.pins.request("4").adapt_to(DigitalSource())) + self.connect(self.gnd, self.conn.pins.request("5").adapt_to(Ground())) + self.connect(self.swo, self.conn.pins.request("6").adapt_to(DigitalBidir())) + + +class SwdCortexTargetTc2050( + SwdCortexTargetConnector, SwdCortexTargetConnectorReset, SwdCortexTargetConnectorSwo, SwdCortexTargetConnectorTdi +): + """UNOFFICIAL tag connect SWD header, maintaining physical pin compatibility with the 2x05 1.27mm header.""" + + @override + def contents(self) -> None: + super().contents() + self.conn = self.Block(TagConnect(10)) + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + self.connect( + self.gnd, + self.conn.pins.request("2").adapt_to(Ground()), + self.conn.pins.request("3").adapt_to(Ground()), + self.conn.pins.request("5").adapt_to(Ground()), + ) + self.connect(self.swd.swdio, self.conn.pins.request("10").adapt_to(DigitalBidir())) + self.connect(self.swd.swclk, self.conn.pins.request("9").adapt_to(DigitalSource())) + self.connect(self.swo, self.conn.pins.request("8").adapt_to(DigitalBidir())) + self.connect(self.tdi, self.conn.pins.request("7").adapt_to(DigitalBidir())) + # TODO: pulldown is a hack to prevent driver conflict warnings, this should be a active low (open drain) driver + self.connect(self.reset, self.conn.pins.request("6").adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) diff --git a/edg/parts/DirectionSwitch_Alps.py b/edg/parts/DirectionSwitch_Alps.py index 440caa559..097e893ec 100644 --- a/edg/parts/DirectionSwitch_Alps.py +++ b/edg/parts/DirectionSwitch_Alps.py @@ -7,22 +7,25 @@ class Skrh(DirectionSwitchCenter, DirectionSwitch, JlcPart, FootprintBlock): """Generic SKRH directional switch with pushbutton. Default part is SKRHABE010, but footprint should be compatible with the entire SKRH series.""" + @override def contents(self) -> None: super().contents() self.footprint( - 'SW', 'edg:DirectionSwitch_Alps_SKRH', + "SW", + "edg:DirectionSwitch_Alps_SKRH", { - '1': self.a, - '2': self.center, - '3': self.c, - '4': self.b, - '5': self.com, - '6': self.d, + "1": self.a, + "2": self.center, + "3": self.c, + "4": self.b, + "5": self.com, + "6": self.d, }, - mfr='Alps Alpine', part='SKRHABE010', - datasheet='https://www.mouser.com/datasheet/2/15/SKRH-1370966.pdf' + mfr="Alps Alpine", + part="SKRHABE010", + datasheet="https://www.mouser.com/datasheet/2/15/SKRH-1370966.pdf", ) - self.assign(self.lcsc_part, 'C139794') + self.assign(self.lcsc_part, "C139794") self.assign(self.actual_basic_part, False) diff --git a/edg/parts/Distance_Vl53l0x.py b/edg/parts/Distance_Vl53l0x.py index 4a43b1914..f27c73ed1 100644 --- a/edg/parts/Distance_Vl53l0x.py +++ b/edg/parts/Distance_Vl53l0x.py @@ -5,135 +5,150 @@ class Vl53l0x_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.6, 3.5) * Volt, - current_draw=(3, 40000) * uAmp # up to 40mA including VCSEL when ranging - ), [Power]) - self.vss = self.Port(Ground(), [Common]) - - # TODO: the datasheet references values to IOVDD, but the value of IOVDD is never stated. - gpio_model = DigitalBidir.from_supply( # This model assumes that IOVDD = Vdd - self.vss, self.vdd, - voltage_limit_abs=(-0.5, 3.6), # not referenced to Vdd! - input_threshold_factor=(0.3, 0.7), - ) - self.xshut = self.Port(DigitalSink.from_bidir(gpio_model)) - self.gpio1 = self.Port(DigitalSource.low_from_supply(self.vss), optional=True) - - # TODO: support addresses, the default is 0x29 though it's software remappable - self.i2c = self.Port(I2cTarget(DigitalBidir.from_supply( - self.vss, self.vdd, - voltage_limit_abs=(-0.5, 3.6), # not referenced to Vdd! - input_threshold_abs=(0.6, 1.12), - )), [Output]) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'OptoDevice:ST_VL53L0X', - { - '1': self.vdd, # AVddVcsel - '2': self.vss, # AVssVcsel - '3': self.vss, # GND - '4': self.vss, # GND2 - '5': self.xshut, - '6': self.vss, # GND3 - '7': self.gpio1, - # '8': , # DNC, must be left floating - '9': self.i2c.sda, - '10': self.i2c.scl, - '11': self.vdd, # AVdd - '12': self.vss, # GND4 - }, - mfr='STMicroelectronics', part='VL53L0X', - datasheet='https://www.st.com/content/ccc/resource/technical/document/datasheet/group3/b2/1e/33/77/c6/92/47/6b/DM00279086/files/DM00279086.pdf/jcr:content/translations/en.DM00279086.pdf' - ) - self.assign(self.lcsc_part, "C91199") - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + + self.vdd = self.Port( + VoltageSink( + voltage_limits=(2.6, 3.5) * Volt, + current_draw=(3, 40000) * uAmp, # up to 40mA including VCSEL when ranging + ), + [Power], + ) + self.vss = self.Port(Ground(), [Common]) + + # TODO: the datasheet references values to IOVDD, but the value of IOVDD is never stated. + gpio_model = DigitalBidir.from_supply( # This model assumes that IOVDD = Vdd + self.vss, + self.vdd, + voltage_limit_abs=(-0.5, 3.6), # not referenced to Vdd! + input_threshold_factor=(0.3, 0.7), + ) + self.xshut = self.Port(DigitalSink.from_bidir(gpio_model)) + self.gpio1 = self.Port(DigitalSource.low_from_supply(self.vss), optional=True) + + # TODO: support addresses, the default is 0x29 though it's software remappable + self.i2c = self.Port( + I2cTarget( + DigitalBidir.from_supply( + self.vss, + self.vdd, + voltage_limit_abs=(-0.5, 3.6), # not referenced to Vdd! + input_threshold_abs=(0.6, 1.12), + ) + ), + [Output], + ) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "OptoDevice:ST_VL53L0X", + { + "1": self.vdd, # AVddVcsel + "2": self.vss, # AVssVcsel + "3": self.vss, # GND + "4": self.vss, # GND2 + "5": self.xshut, + "6": self.vss, # GND3 + "7": self.gpio1, + # '8': , # DNC, must be left floating + "9": self.i2c.sda, + "10": self.i2c.scl, + "11": self.vdd, # AVdd + "12": self.vss, # GND4 + }, + mfr="STMicroelectronics", + part="VL53L0X", + datasheet="https://www.st.com/content/ccc/resource/technical/document/datasheet/group3/b2/1e/33/77/c6/92/47/6b/DM00279086/files/DM00279086.pdf/jcr:content/translations/en.DM00279086.pdf", + ) + self.assign(self.lcsc_part, "C91199") + self.assign(self.actual_basic_part, False) class Vl53l0x(DistanceSensor, Resettable, GeneratorBlock): - """Time-of-flight laser ranging sensor, up to 2m""" - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Vl53l0x_Device()) - self.gnd = self.Export(self.ic.vss, [Common]) - self.pwr = self.Export(self.ic.vdd, [Power]) + """Time-of-flight laser ranging sensor, up to 2m""" - self.i2c = self.Export(self.ic.i2c) + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Vl53l0x_Device()) + self.gnd = self.Export(self.ic.vss, [Common]) + self.pwr = self.Export(self.ic.vdd, [Power]) - self.int = self.Port(DigitalSource.empty(), optional=True, - doc="Interrupt output for new data available") - self.generator_param(self.reset.is_connected(), self.int.is_connected()) + self.i2c = self.Export(self.ic.i2c) - @override - def generate(self) -> None: - super().generate() - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.xshut) - else: - self.connect(self.pwr.as_digital_source(), self.ic.xshut) + self.int = self.Port(DigitalSource.empty(), optional=True, doc="Interrupt output for new data available") + self.generator_param(self.reset.is_connected(), self.int.is_connected()) - if self.get(self.int.is_connected()): - self.connect(self.int, self.ic.gpio1) + @override + def generate(self) -> None: + super().generate() + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.xshut) + else: + self.connect(self.pwr.as_digital_source(), self.ic.xshut) - # Datasheet Figure 3, two decoupling capacitors - self.vdd_cap = ElementDict[DecouplingCapacitor]() - self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.vdd_cap[1] = self.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + if self.get(self.int.is_connected()): + self.connect(self.int, self.ic.gpio1) + + # Datasheet Figure 3, two decoupling capacitors + self.vdd_cap = ElementDict[DecouplingCapacitor]() + self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap[1] = self.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) class Vl53l0xConnector(Vl53l0x, WrapperFootprintBlock): - """Connector to an external VL53L0X breakout board. - Uses the pinout from the Adafruit product: https://www.adafruit.com/product/3317 - This has an onboard 2.8v regulator, but thankfully the IO tolerance is not referenced to Vdd - - TODO: not completely correct that this should extend the application circuit""" - @override - def generate(self) -> None: - super().generate() - self.footprint( - 'J', 'Connector_PinSocket_2.54mm:PinSocket_1x06_P2.54mm_Vertical', - { - '1': self.pwr, - '2': self.gnd, - '3': self.i2c.scl, - '4': self.i2c.sda, - '5': self.ic.gpio1, - '6': self.ic.xshut - }, - ) + """Connector to an external VL53L0X breakout board. + Uses the pinout from the Adafruit product: https://www.adafruit.com/product/3317 + This has an onboard 2.8v regulator, but thankfully the IO tolerance is not referenced to Vdd + + TODO: not completely correct that this should extend the application circuit""" + + @override + def generate(self) -> None: + super().generate() + self.footprint( + "J", + "Connector_PinSocket_2.54mm:PinSocket_1x06_P2.54mm_Vertical", + { + "1": self.pwr, + "2": self.gnd, + "3": self.i2c.scl, + "4": self.i2c.sda, + "5": self.ic.gpio1, + "6": self.ic.xshut, + }, + ) class Vl53l0xArray(DistanceSensor, GeneratorBlock): - """Array of Vl53l0x with common I2C but individually exposed XSHUT pins and optionally GPIO1 (interrupt).""" - def __init__(self, count: IntLike, *, first_reset_fixed: BoolLike = False): - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - self.i2c = self.Port(I2cTarget.empty()) - self.reset = self.Port(Vector(DigitalSink.empty())) - # TODO better support for optional vectors so the inner doesn't connect if the outer doesn't connect - # self.int = self.Port(Vector(DigitalSingleSource.empty()), optional=True) - - self.count = self.ArgParameter(count) - self.first_reset_fixed = self.ArgParameter(first_reset_fixed) - self.generator_param(self.count, self.first_reset_fixed) - - @override - def generate(self) -> None: - super().generate() - self.elt = ElementDict[Vl53l0x]() - for elt_i in range(self.get(self.count)): - elt = self.elt[str(elt_i)] = self.Block(Vl53l0x()) - self.connect(self.pwr, elt.pwr) - self.connect(self.gnd, elt.gnd) - self.connect(self.i2c, elt.i2c) - if self.get(self.first_reset_fixed) and elt_i == 0: - self.connect(elt.pwr.as_digital_source(), elt.reset) - else: - self.connect(self.reset.append_elt(DigitalSink.empty(), str(elt_i)), elt.reset) + """Array of Vl53l0x with common I2C but individually exposed XSHUT pins and optionally GPIO1 (interrupt).""" + + def __init__(self, count: IntLike, *, first_reset_fixed: BoolLike = False): + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + self.i2c = self.Port(I2cTarget.empty()) + self.reset = self.Port(Vector(DigitalSink.empty())) + # TODO better support for optional vectors so the inner doesn't connect if the outer doesn't connect + # self.int = self.Port(Vector(DigitalSingleSource.empty()), optional=True) + + self.count = self.ArgParameter(count) + self.first_reset_fixed = self.ArgParameter(first_reset_fixed) + self.generator_param(self.count, self.first_reset_fixed) + + @override + def generate(self) -> None: + super().generate() + self.elt = ElementDict[Vl53l0x]() + for elt_i in range(self.get(self.count)): + elt = self.elt[str(elt_i)] = self.Block(Vl53l0x()) + self.connect(self.pwr, elt.pwr) + self.connect(self.gnd, elt.gnd) + self.connect(self.i2c, elt.i2c) + if self.get(self.first_reset_fixed) and elt_i == 0: + self.connect(elt.pwr.as_digital_source(), elt.reset) + else: + self.connect(self.reset.append_elt(DigitalSink.empty(), str(elt_i)), elt.reset) diff --git a/edg/parts/EInkBoostPowerPath.py b/edg/parts/EInkBoostPowerPath.py index 24ec74a0e..01818816b 100644 --- a/edg/parts/EInkBoostPowerPath.py +++ b/edg/parts/EInkBoostPowerPath.py @@ -4,78 +4,79 @@ class EInkBoostPowerPath(Interface, KiCadSchematicBlock): - """Boost converter power path for e-ink displays with negative voltage generation through - a bootstrap switched-cap circuit. - Current is the peak current through the FET and diodes.""" - def __init__(self, voltage_out: RangeLike, current: RangeLike, inductance: RangeLike, - in_capacitance: RangeLike, out_capacitance: RangeLike, resistance: RangeLike, - diode_voltage_drop: RangeLike = (0, 0.5)*Volt): - super().__init__() - self.gnd = self.Port(Ground.empty()) - self.pwr_in = self.Port(VoltageSink.empty()) - self.pos_out = self.Port(VoltageSource.empty()) - self.neg_out = self.Port(VoltageSource.empty()) + """Boost converter power path for e-ink displays with negative voltage generation through + a bootstrap switched-cap circuit. + Current is the peak current through the FET and diodes.""" - self.gate = self.Port(Passive.empty()) - self.isense = self.Port(Passive.empty(), optional=True) + def __init__( + self, + voltage_out: RangeLike, + current: RangeLike, + inductance: RangeLike, + in_capacitance: RangeLike, + out_capacitance: RangeLike, + resistance: RangeLike, + diode_voltage_drop: RangeLike = (0, 0.5) * Volt, + ): + super().__init__() + self.gnd = self.Port(Ground.empty()) + self.pwr_in = self.Port(VoltageSink.empty()) + self.pos_out = self.Port(VoltageSource.empty()) + self.neg_out = self.Port(VoltageSource.empty()) - self.voltage_out = self.ArgParameter(voltage_out) - self.current = self.ArgParameter(current) - self.inductance = self.ArgParameter(inductance) - self.in_capacitance = self.ArgParameter(in_capacitance) - self.out_capacitance = self.ArgParameter(out_capacitance) - self.resistance = self.ArgParameter(resistance) - self.diode_voltage_drop = self.ArgParameter(diode_voltage_drop) + self.gate = self.Port(Passive.empty()) + self.isense = self.Port(Passive.empty(), optional=True) - @override - def contents(self) -> None: - super().contents() + self.voltage_out = self.ArgParameter(voltage_out) + self.current = self.ArgParameter(current) + self.inductance = self.ArgParameter(inductance) + self.in_capacitance = self.ArgParameter(in_capacitance) + self.out_capacitance = self.ArgParameter(out_capacitance) + self.resistance = self.ArgParameter(resistance) + self.diode_voltage_drop = self.ArgParameter(diode_voltage_drop) - self.fet = self.Block(Fet.NFet( - drain_voltage=self.voltage_out.hull((0, 0)*Volt), - drain_current=self.current, - gate_voltage=(0, 5)*Volt, - rds_on=(0, 400)*mOhm - )) - self.inductor = self.Block(Inductor( - inductance=self.inductance, - current=self.current - )) - self.sense = self.Block(Resistor( - resistance=self.resistance - )) - self.in_cap = self.Block(DecouplingCapacitor( - capacitance=self.in_capacitance - )) + @override + def contents(self) -> None: + super().contents() - diode_model = Diode( - reverse_voltage=self.voltage_out.hull((0, 0)*Volt), current=self.current.hull((0, 0)*Volt), - voltage_drop=self.diode_voltage_drop, - reverse_recovery_time=(0, 500e-9) # guess from Digikey's classification for "fast recovery" - ) - self.diode = self.Block(diode_model) - self.boot_neg_diode = self.Block(diode_model) - self.boot_gnd_diode = self.Block(diode_model) + self.fet = self.Block( + Fet.NFet( + drain_voltage=self.voltage_out.hull((0, 0) * Volt), + drain_current=self.current, + gate_voltage=(0, 5) * Volt, + rds_on=(0, 400) * mOhm, + ) + ) + self.inductor = self.Block(Inductor(inductance=self.inductance, current=self.current)) + self.sense = self.Block(Resistor(resistance=self.resistance)) + self.in_cap = self.Block(DecouplingCapacitor(capacitance=self.in_capacitance)) - self.boot_cap = self.Block(Capacitor( - capacitance=self.out_capacitance, voltage=self.voltage_out - )) - out_cap_model = DecouplingCapacitor( - capacitance=self.out_capacitance - ) - self.out_cap = self.Block(out_cap_model) - self.neg_out_cap = self.Block(out_cap_model) + diode_model = Diode( + reverse_voltage=self.voltage_out.hull((0, 0) * Volt), + current=self.current.hull((0, 0) * Volt), + voltage_drop=self.diode_voltage_drop, + reverse_recovery_time=(0, 500e-9), # guess from Digikey's classification for "fast recovery" + ) + self.diode = self.Block(diode_model) + self.boot_neg_diode = self.Block(diode_model) + self.boot_gnd_diode = self.Block(diode_model) - self.import_kicad( - self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'inductor.1': VoltageSink(), - 'diode.K': VoltageSource( - voltage_out=self.voltage_out, - ), - 'boot_neg_diode.A': VoltageSource( - voltage_out=-self.voltage_out, - ), - 'boot_gnd_diode.K': Ground(), - 'sense.2': Ground(), - }) + self.boot_cap = self.Block(Capacitor(capacitance=self.out_capacitance, voltage=self.voltage_out)) + out_cap_model = DecouplingCapacitor(capacitance=self.out_capacitance) + self.out_cap = self.Block(out_cap_model) + self.neg_out_cap = self.Block(out_cap_model) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "inductor.1": VoltageSink(), + "diode.K": VoltageSource( + voltage_out=self.voltage_out, + ), + "boot_neg_diode.A": VoltageSource( + voltage_out=-self.voltage_out, + ), + "boot_gnd_diode.K": Ground(), + "sense.2": Ground(), + }, + ) diff --git a/edg/parts/EInk_E2154fs091.py b/edg/parts/EInk_E2154fs091.py index 08befc1ee..21d9a8939 100644 --- a/edg/parts/EInk_E2154fs091.py +++ b/edg/parts/EInk_E2154fs091.py @@ -6,171 +6,183 @@ class E2154fs091_Device(InternalSubcircuit, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSink( - voltage_limits=(2.4, 3.6)*Volt, current_draw=(0, 10)*mAmp - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - - dio_model = DigitalBidir( - voltage_limits=(-0, self.pwr.link().voltage.upper()), # TODO needs some tolerance - current_draw=(0, 0), - voltage_out=(0, self.pwr.link().voltage.lower()), - current_limits=(-400, 400) * uAmp, - input_thresholds=(0.3 * self.pwr.link().voltage.upper(), - 0.7 * self.pwr.link().voltage.upper()), - output_thresholds=(0.4, self.pwr.link().voltage.upper() - 0.4), - ) - - self.busy = self.Port(DigitalSource.from_bidir(dio_model)) - self.reset = self.Port(DigitalSink.from_bidir(dio_model)) - self.dc = self.Port(DigitalSink.from_bidir(dio_model)) - self.cs = self.Port(DigitalSink.from_bidir(dio_model)) - self.spi = self.Port(SpiPeripheral(model=dio_model)) - - # TODO model all these parts, then fix all the Passive connections - self.gdr = self.Port(Passive()) - self.rese = self.Port(Passive()) - self.vslr = self.Port(Passive()) - self.vdhr = self.Port(Passive()) - self.vddd = self.Port(Passive()) - self.vdh = self.Port(Passive()) - self.vgh = self.Port(Passive()) - self.vdl = self.Port(Passive()) - self.vgl = self.Port(Passive()) - self.vcom = self.Port(Passive()) - - @override - def contents(self) -> None: - super().contents() - - pinning: Dict[str, CircuitPort] = { - # '1': , # NC - '2': self.gdr, - '3': self.rese, - '4': self.vslr, # NC / VSLR - for compatibility w/ Waveshare units - '5': self.vdhr, # NC / VSL - for compatibility w/ Waveshare units - # '6': , # TSCL - # '7': , # TSDA - '8': self.gnd, # BS, to set panel interface - '9': self.busy, - '10': self.reset, # active-low reset - '11': self.dc, # data/control - '12': self.cs, - '13': self.spi.sck, - '14': self.spi.mosi, - '15': self.pwr, # VddIO - '16': self.pwr, # Vdd - '17': self.gnd, - '18': self.vddd, - # '19': , # VPP, for OTP programming - '20': self.vdh, - '21': self.vgh, - '22': self.vdl, - '23': self.vgl, - '24': self.vcom, - } - - self.footprint( - 'U', 'Connector_FFC-FPC:Hirose_FH12-24S-0.5SH_1x24-1MP_P0.50mm_Horizontal', # TODO EPD outline - {str(25-int(pin)): port for pin, port in pinning.items()}, # reversed since the FPC contacts face down - part='E2154FS091, FH12-24S-0.5SH(55)', # TODO multiple parts - datasheet='https://www.pervasivedisplays.com/wp-content/uploads/2019/06/1P200-00_01_E2154CS091_24PINFPC_20180807-3.pdf', - ) - # also, for the correct PN: 'https://download.siliconexpert.com/pdfs/2017/11/7/8/22/5/959/pervas_/manual/1p165-00_01_e2154fs091_24pinfpc_20171026.pdf' - # trying for compatibility with https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT_(B) / https://www.waveshare.com/w/upload/d/d8/2.7inch-e-paper-b-specification.pdf - # other connectors specified by datasheet: STARCONN 6700S24 "or Compatible" + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink(voltage_limits=(2.4, 3.6) * Volt, current_draw=(0, 10) * mAmp), [Power]) + self.gnd = self.Port(Ground(), [Common]) + + dio_model = DigitalBidir( + voltage_limits=(-0, self.pwr.link().voltage.upper()), # TODO needs some tolerance + current_draw=(0, 0), + voltage_out=(0, self.pwr.link().voltage.lower()), + current_limits=(-400, 400) * uAmp, + input_thresholds=(0.3 * self.pwr.link().voltage.upper(), 0.7 * self.pwr.link().voltage.upper()), + output_thresholds=(0.4, self.pwr.link().voltage.upper() - 0.4), + ) + + self.busy = self.Port(DigitalSource.from_bidir(dio_model)) + self.reset = self.Port(DigitalSink.from_bidir(dio_model)) + self.dc = self.Port(DigitalSink.from_bidir(dio_model)) + self.cs = self.Port(DigitalSink.from_bidir(dio_model)) + self.spi = self.Port(SpiPeripheral(model=dio_model)) + + # TODO model all these parts, then fix all the Passive connections + self.gdr = self.Port(Passive()) + self.rese = self.Port(Passive()) + self.vslr = self.Port(Passive()) + self.vdhr = self.Port(Passive()) + self.vddd = self.Port(Passive()) + self.vdh = self.Port(Passive()) + self.vgh = self.Port(Passive()) + self.vdl = self.Port(Passive()) + self.vgl = self.Port(Passive()) + self.vcom = self.Port(Passive()) + + @override + def contents(self) -> None: + super().contents() + + pinning: Dict[str, CircuitPort] = { + # '1': , # NC + "2": self.gdr, + "3": self.rese, + "4": self.vslr, # NC / VSLR - for compatibility w/ Waveshare units + "5": self.vdhr, # NC / VSL - for compatibility w/ Waveshare units + # '6': , # TSCL + # '7': , # TSDA + "8": self.gnd, # BS, to set panel interface + "9": self.busy, + "10": self.reset, # active-low reset + "11": self.dc, # data/control + "12": self.cs, + "13": self.spi.sck, + "14": self.spi.mosi, + "15": self.pwr, # VddIO + "16": self.pwr, # Vdd + "17": self.gnd, + "18": self.vddd, + # '19': , # VPP, for OTP programming + "20": self.vdh, + "21": self.vgh, + "22": self.vdl, + "23": self.vgl, + "24": self.vcom, + } + + self.footprint( + "U", + "Connector_FFC-FPC:Hirose_FH12-24S-0.5SH_1x24-1MP_P0.50mm_Horizontal", # TODO EPD outline + {str(25 - int(pin)): port for pin, port in pinning.items()}, # reversed since the FPC contacts face down + part="E2154FS091, FH12-24S-0.5SH(55)", # TODO multiple parts + datasheet="https://www.pervasivedisplays.com/wp-content/uploads/2019/06/1P200-00_01_E2154CS091_24PINFPC_20180807-3.pdf", + ) + # also, for the correct PN: 'https://download.siliconexpert.com/pdfs/2017/11/7/8/22/5/959/pervas_/manual/1p165-00_01_e2154fs091_24pinfpc_20171026.pdf' + # trying for compatibility with https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT_(B) / https://www.waveshare.com/w/upload/d/d8/2.7inch-e-paper-b-specification.pdf + # other connectors specified by datasheet: STARCONN 6700S24 "or Compatible" class E2154fs091(EInk): - """1.54" 152x152px red/black/white e-ink display with 24-pin FPC connector, 0.5mm pitch""" - def __init__(self) -> None: - super().__init__() - - self.ic = self.Block(E2154fs091_Device()) - self.pwr = self.Export(self.ic.pwr, [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - - self.busy = self.Export(self.ic.busy) - self.reset = self.Export(self.ic.reset) - self.dc = self.Export(self.ic.dc) - self.cs = self.Export(self.ic.cs) - self.spi = self.Export(self.ic.spi) - - @override - def contents(self) -> None: - super().contents() - - self.boost_sw = self.Block(Fet.NFet( # TODO should use switching NFet, but we don't know anything about frequency - drain_voltage=(0, 30)*Volt, - drain_current=(0, 0.8)*Amp, # assumed, from inductor rating - gate_voltage=(3, 16)*Volt, # assumed, from capacitor ratings # TODO use pwr voltage instead of hardcoding - rds_on=(0, 85)*mOhm, - power=(0, 0.2)*Watt # about 4x resistive loss @ 0.8A, 85mOhm; we don't know the switch frequency or drive current - )) - self.boost_ind = self.Block(Inductor( - inductance=10*uHenry(tol=0.2), current=(0, 0.8)*Amp, frequency=100*kHertz(tol=0) - )) - self.boost_res = self.Block(Resistor( - resistance=0.47*Ohm(tol=0.01), power=0*Watt(tol=0) # TODO actual power numbers based on current draw - )) - self.boot_cap = self.Block(Capacitor( - capacitance=(4.7, float('inf'))*uFarad, voltage=(0, 16)*Volt # lower-bounded so it can be derated - )) - self.connect(self.pwr, self.boost_ind.a.adapt_to(VoltageSink())) - self.connect(self.boost_ind.b, self.boost_sw.drain, self.boot_cap.pos) - self.connect(self.boost_sw.gate, self.ic.gdr) - self.connect(self.boost_sw.source, self.boost_res.a, self.ic.rese) - self.connect(self.boost_res.b.adapt_to(Ground()), self.gnd) - - self.vdd_cap0 = self.Block(DecouplingCapacitor(capacitance=0.11*uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.vdd_cap1 = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) - - decoupling_cap_model = Capacitor( - capacitance=1*uFarad(tol=0.2), voltage=(0, 16)*Volt - ) - - # TODO make these actually DecouplingCaps? - self.vslr_cap = self.Block(decoupling_cap_model) - self.connect(self.ic.vslr, self.vslr_cap.pos) - self.vdhr_cap = self.Block(decoupling_cap_model) - self.connect(self.ic.vdhr, self.vdhr_cap.pos) - self.vddd_cap = self.Block(decoupling_cap_model) - self.connect(self.ic.vddd, self.vddd_cap.pos) - self.vdh_cap = self.Block(decoupling_cap_model) - self.connect(self.ic.vdh, self.vdh_cap.pos) - self.vgh_cap = self.Block(decoupling_cap_model) - self.connect(self.ic.vgh, self.vgh_cap.pos) - self.vdl_cap = self.Block(decoupling_cap_model) - self.connect(self.ic.vdl, self.vdl_cap.pos) - self.vgl_cap = self.Block(decoupling_cap_model) - self.connect(self.ic.vgl, self.vgl_cap.pos) - self.vcom_cap = self.Block(decoupling_cap_model) - self.connect(self.ic.vcom, self.vcom_cap.pos) - - self.connect(self.gnd, - self.vslr_cap.neg.adapt_to(Ground()), - self.vdhr_cap.neg.adapt_to(Ground()), - self.vddd_cap.neg.adapt_to(Ground()), - self.vdh_cap.neg.adapt_to(Ground()), - self.vgh_cap.neg.adapt_to(Ground()), - self.vdl_cap.neg.adapt_to(Ground()), - self.vgl_cap.neg.adapt_to(Ground()), - self.vcom_cap.neg.adapt_to(Ground())) - - diode_model = Diode( - reverse_voltage=(0, 25)*Volt, current=(0, 2)*Amp, voltage_drop=(0, 0.5)*Volt, - reverse_recovery_time=(0, 500e-9) # guess from Digikey's classification for "fast recovery" - ) - - self.boost_dio = self.Block(diode_model) - self.connect(self.boost_ind.b, self.boost_dio.anode) - self.connect(self.boost_dio.cathode, self.vgh_cap.pos) - self.vgl_dio = self.Block(diode_model) - self.connect(self.vgl_cap.pos, self.vgl_dio.anode) - self.connect(self.vgl_dio.cathode, self.boot_cap.neg) - self.boot_dio = self.Block(diode_model) - self.connect(self.boot_cap.neg, self.boot_dio.anode) - self.connect(self.boot_dio.cathode.adapt_to(Ground()), self.gnd) + """1.54" 152x152px red/black/white e-ink display with 24-pin FPC connector, 0.5mm pitch""" + + def __init__(self) -> None: + super().__init__() + + self.ic = self.Block(E2154fs091_Device()) + self.pwr = self.Export(self.ic.pwr, [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + + self.busy = self.Export(self.ic.busy) + self.reset = self.Export(self.ic.reset) + self.dc = self.Export(self.ic.dc) + self.cs = self.Export(self.ic.cs) + self.spi = self.Export(self.ic.spi) + + @override + def contents(self) -> None: + super().contents() + + self.boost_sw = self.Block( + Fet.NFet( # TODO should use switching NFet, but we don't know anything about frequency + drain_voltage=(0, 30) * Volt, + drain_current=(0, 0.8) * Amp, # assumed, from inductor rating + gate_voltage=(3, 16) + * Volt, # assumed, from capacitor ratings # TODO use pwr voltage instead of hardcoding + rds_on=(0, 85) * mOhm, + power=(0, 0.2) + * Watt, # about 4x resistive loss @ 0.8A, 85mOhm; we don't know the switch frequency or drive current + ) + ) + self.boost_ind = self.Block( + Inductor(inductance=10 * uHenry(tol=0.2), current=(0, 0.8) * Amp, frequency=100 * kHertz(tol=0)) + ) + self.boost_res = self.Block( + Resistor( + resistance=0.47 * Ohm(tol=0.01), + power=0 * Watt(tol=0), # TODO actual power numbers based on current draw + ) + ) + self.boot_cap = self.Block( + Capacitor( + capacitance=(4.7, float("inf")) * uFarad, voltage=(0, 16) * Volt # lower-bounded so it can be derated + ) + ) + self.connect(self.pwr, self.boost_ind.a.adapt_to(VoltageSink())) + self.connect(self.boost_ind.b, self.boost_sw.drain, self.boot_cap.pos) + self.connect(self.boost_sw.gate, self.ic.gdr) + self.connect(self.boost_sw.source, self.boost_res.a, self.ic.rese) + self.connect(self.boost_res.b.adapt_to(Ground()), self.gnd) + + self.vdd_cap0 = self.Block(DecouplingCapacitor(capacitance=0.11 * uFarad(tol=0.2))).connected( + self.gnd, self.pwr + ) + self.vdd_cap1 = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + + decoupling_cap_model = Capacitor(capacitance=1 * uFarad(tol=0.2), voltage=(0, 16) * Volt) + + # TODO make these actually DecouplingCaps? + self.vslr_cap = self.Block(decoupling_cap_model) + self.connect(self.ic.vslr, self.vslr_cap.pos) + self.vdhr_cap = self.Block(decoupling_cap_model) + self.connect(self.ic.vdhr, self.vdhr_cap.pos) + self.vddd_cap = self.Block(decoupling_cap_model) + self.connect(self.ic.vddd, self.vddd_cap.pos) + self.vdh_cap = self.Block(decoupling_cap_model) + self.connect(self.ic.vdh, self.vdh_cap.pos) + self.vgh_cap = self.Block(decoupling_cap_model) + self.connect(self.ic.vgh, self.vgh_cap.pos) + self.vdl_cap = self.Block(decoupling_cap_model) + self.connect(self.ic.vdl, self.vdl_cap.pos) + self.vgl_cap = self.Block(decoupling_cap_model) + self.connect(self.ic.vgl, self.vgl_cap.pos) + self.vcom_cap = self.Block(decoupling_cap_model) + self.connect(self.ic.vcom, self.vcom_cap.pos) + + self.connect( + self.gnd, + self.vslr_cap.neg.adapt_to(Ground()), + self.vdhr_cap.neg.adapt_to(Ground()), + self.vddd_cap.neg.adapt_to(Ground()), + self.vdh_cap.neg.adapt_to(Ground()), + self.vgh_cap.neg.adapt_to(Ground()), + self.vdl_cap.neg.adapt_to(Ground()), + self.vgl_cap.neg.adapt_to(Ground()), + self.vcom_cap.neg.adapt_to(Ground()), + ) + + diode_model = Diode( + reverse_voltage=(0, 25) * Volt, + current=(0, 2) * Amp, + voltage_drop=(0, 0.5) * Volt, + reverse_recovery_time=(0, 500e-9), # guess from Digikey's classification for "fast recovery" + ) + + self.boost_dio = self.Block(diode_model) + self.connect(self.boost_ind.b, self.boost_dio.anode) + self.connect(self.boost_dio.cathode, self.vgh_cap.pos) + self.vgl_dio = self.Block(diode_model) + self.connect(self.vgl_cap.pos, self.vgl_dio.anode) + self.connect(self.vgl_dio.cathode, self.boot_cap.neg) + self.boot_dio = self.Block(diode_model) + self.connect(self.boot_cap.neg, self.boot_dio.anode) + self.connect(self.boot_dio.cathode.adapt_to(Ground()), self.gnd) diff --git a/edg/parts/EInk_Er_Epd027_2.py b/edg/parts/EInk_Er_Epd027_2.py index d6972036f..1878e94a4 100644 --- a/edg/parts/EInk_Er_Epd027_2.py +++ b/edg/parts/EInk_Er_Epd027_2.py @@ -9,75 +9,102 @@ class Er_Epd027_2_Outline(InternalSubcircuit, FootprintBlock): @override def contents(self) -> None: super().contents() - self.footprint('U', 'edg:Lcd_Er_Epd027_2_Outline', {}, - 'EastRising', 'ER-EPD027-2', - datasheet='https://www.buydisplay.com/download/manual/ER-EPD027-2_datasheet.pdf') + self.footprint( + "U", + "edg:Lcd_Er_Epd027_2_Outline", + {}, + "EastRising", + "ER-EPD027-2", + datasheet="https://www.buydisplay.com/download/manual/ER-EPD027-2_datasheet.pdf", + ) class Er_Epd027_2_Device(InternalSubcircuit, Block): """24-pin FPC connector for the ER-EPD-27-2 device""" + def __init__(self) -> None: super().__init__() self.conn = self.Block(Fpc050Bottom(length=24)) - self.vss = self.Export(self.conn.pins.request('17').adapt_to(Ground()), [Common]) - self.vdd = self.Export(self.conn.pins.request('16').adapt_to(VoltageSink( - voltage_limits=(2.5, 3.7)*Volt, # VCI specs, assumed for all logic - current_draw=(0.001, 2.1)*mAmp # sleep max to operating typ - ))) - self.vddio = self.Export(self.conn.pins.request('15').adapt_to(VoltageSink( - voltage_limits=(2.5, 3.7)*Volt, # VCI specs, assumed for all logic - ))) - self.vdd1v8 = self.Export(self.conn.pins.request('18').adapt_to(VoltageSource( - voltage_out=1.8*Volt(tol=0), # specs not given - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - - din_model = DigitalSink.from_supply( - self.vss, self.vddio, - input_threshold_factor=(0.2, 0.8) + self.vss = self.Export(self.conn.pins.request("17").adapt_to(Ground()), [Common]) + self.vdd = self.Export( + self.conn.pins.request("16").adapt_to( + VoltageSink( + voltage_limits=(2.5, 3.7) * Volt, # VCI specs, assumed for all logic + current_draw=(0.001, 2.1) * mAmp, # sleep max to operating typ + ) + ) + ) + self.vddio = self.Export( + self.conn.pins.request("15").adapt_to( + VoltageSink( + voltage_limits=(2.5, 3.7) * Volt, # VCI specs, assumed for all logic + ) + ) ) + self.vdd1v8 = self.Export( + self.conn.pins.request("18").adapt_to( + VoltageSource( + voltage_out=1.8 * Volt(tol=0), # specs not given + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) + + din_model = DigitalSink.from_supply(self.vss, self.vddio, input_threshold_factor=(0.2, 0.8)) - self.gdr = self.Export(self.conn.pins.request('2')) - self.rese = self.Export(self.conn.pins.request('3')) + self.gdr = self.Export(self.conn.pins.request("2")) + self.rese = self.Export(self.conn.pins.request("3")) # pin 4 is NC for this part - self.vshr = self.Export(self.conn.pins.request('5').adapt_to(VoltageSource( - voltage_out=(0, 11)*Volt, # inferred from power selection register - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - self.vsh = self.Export(self.conn.pins.request('20').adapt_to(VoltageSource( - voltage_out=(2.4, 15)*Volt, # inferred from power selection register - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - self.vsl = self.Export(self.conn.pins.request('22').adapt_to(VoltageSource( - voltage_out=(-15, -2.4)*Volt, # inferred from power selection register - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - - self.vgh = self.Export(self.conn.pins.request('21').adapt_to(VoltageSink( - voltage_limits=(13, 20)*Volt - ))) - self.vgl = self.Export(self.conn.pins.request('23').adapt_to(VoltageSink( - voltage_limits=(-20, -13)*Volt - ))) - - self.bs = self.Export(self.conn.pins.request('8').adapt_to(din_model)) - self.busy = self.Export(self.conn.pins.request('9').adapt_to(din_model), optional=True) - self.rst = self.Export(self.conn.pins.request('10').adapt_to(din_model)) - self.dc = self.Export(self.conn.pins.request('11').adapt_to(din_model), optional=True) - self.csb = self.Export(self.conn.pins.request('12').adapt_to(din_model)) + self.vshr = self.Export( + self.conn.pins.request("5").adapt_to( + VoltageSource( + voltage_out=(0, 11) * Volt, # inferred from power selection register + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) + self.vsh = self.Export( + self.conn.pins.request("20").adapt_to( + VoltageSource( + voltage_out=(2.4, 15) * Volt, # inferred from power selection register + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) + self.vsl = self.Export( + self.conn.pins.request("22").adapt_to( + VoltageSource( + voltage_out=(-15, -2.4) * Volt, # inferred from power selection register + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) + + self.vgh = self.Export(self.conn.pins.request("21").adapt_to(VoltageSink(voltage_limits=(13, 20) * Volt))) + self.vgl = self.Export(self.conn.pins.request("23").adapt_to(VoltageSink(voltage_limits=(-20, -13) * Volt))) + + self.bs = self.Export(self.conn.pins.request("8").adapt_to(din_model)) + self.busy = self.Export(self.conn.pins.request("9").adapt_to(din_model), optional=True) + self.rst = self.Export(self.conn.pins.request("10").adapt_to(din_model)) + self.dc = self.Export(self.conn.pins.request("11").adapt_to(din_model), optional=True) + self.csb = self.Export(self.conn.pins.request("12").adapt_to(din_model)) self.spi = self.Port(SpiPeripheral.empty()) - self.connect(self.spi.sck, self.conn.pins.request('13').adapt_to(din_model)) # SCL - self.connect(self.spi.mosi, self.conn.pins.request('14').adapt_to(din_model)) # SDA + self.connect(self.spi.sck, self.conn.pins.request("13").adapt_to(din_model)) # SCL + self.connect(self.spi.mosi, self.conn.pins.request("14").adapt_to(din_model)) # SDA self.miso_nc = self.Block(DigitalBidirNotConnected()) self.connect(self.spi.miso, self.miso_nc.port) - self.vcom = self.Export(self.conn.pins.request('24').adapt_to(VoltageSource( - voltage_out=(2.4, 20)*Volt, # configurable up to VGH - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) + self.vcom = self.Export( + self.conn.pins.request("24").adapt_to( + VoltageSource( + voltage_out=(2.4, 20) * Volt, # configurable up to VGH + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) class Er_Epd027_2(EInk, GeneratorBlock): @@ -85,6 +112,7 @@ class Er_Epd027_2(EInk, GeneratorBlock): (Probably) compatible with https://www.waveshare.com/w/upload/b/ba/2.7inch_e-Paper_V2_Specification.pdf, and https://www.waveshare.com/w/upload/7/7b/2.7inch-e-paper-b-v2-specification.pdf """ + def __init__(self) -> None: super().__init__() self.device = self.Block(Er_Epd027_2_Device()) @@ -104,25 +132,39 @@ def contents(self) -> None: self.lcd = self.Block(Er_Epd027_2_Outline()) # for device outline - self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vdd) + self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd + ) self.connect(self.device.vdd, self.device.vddio) - self.vdd1v8_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vdd1v8) + self.vdd1v8_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd1v8 + ) - self.vsh_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vsh) - self.vshr_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vshr) - self.vsl_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vsl) - self.vcom_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vcom) + self.vsh_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vsh + ) + self.vshr_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vshr + ) + self.vsl_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vsl + ) + self.vcom_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcom + ) # current limit based on 200mA saturation current of reference inductor - self.boost = self.Block(EInkBoostPowerPath(20*Volt(tol=0), (0, 200)*mAmp, 47*uHenry(tol=0.2), - 1*uFarad(tol=0.2), 4.7*uFarad(tol=0.2), 2.2*Ohm(tol=0.01))) + self.boost = self.Block( + EInkBoostPowerPath( + 20 * Volt(tol=0), + (0, 200) * mAmp, + 47 * uHenry(tol=0.2), + 1 * uFarad(tol=0.2), + 4.7 * uFarad(tol=0.2), + 2.2 * Ohm(tol=0.01), + ) + ) self.connect(self.gnd, self.boost.gnd) self.connect(self.pwr, self.boost.pwr_in) self.connect(self.device.gdr, self.boost.gate) diff --git a/edg/parts/EInk_WaveshareDriver.py b/edg/parts/EInk_WaveshareDriver.py index 3fb2527fb..c2b1e3e7f 100644 --- a/edg/parts/EInk_WaveshareDriver.py +++ b/edg/parts/EInk_WaveshareDriver.py @@ -7,72 +7,97 @@ class Waveshare_Epd_Device(InternalSubcircuit, Block): """24-pin FPC connector compatible across multiple EPDs""" + def __init__(self) -> None: super().__init__() self.conn = self.Block(Fpc050Bottom(length=24)) - self.vss = self.Export(self.conn.pins.request('17').adapt_to(Ground()), [Common]) - self.vdd = self.Export(self.conn.pins.request('16').adapt_to(VoltageSink( - voltage_limits=(2.5, 3.7)*Volt, # VCI specs, assumed for all logic - current_draw=(0.001, 2.1)*mAmp # sleep max to operating typ - ))) - self.vddio = self.Export(self.conn.pins.request('15').adapt_to(VoltageSink( - voltage_limits=(2.5, 3.7)*Volt, # VCI specs, assumed for all logic - ))) - self.vdd1v8 = self.Export(self.conn.pins.request('18').adapt_to(VoltageSource( - voltage_out=1.8*Volt(tol=0), # specs not given - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - - din_model = DigitalSink.from_supply( - self.vss, self.vddio, - input_threshold_factor=(0.2, 0.8) - ) - - self.gdr = self.Export(self.conn.pins.request('2')) - self.rese = self.Export(self.conn.pins.request('3')) - - self.vgl = self.Export(self.conn.pins.request('4').adapt_to(VoltageSource( - voltage_out=(-15, -2.5)*Volt, # inferred from power selection register - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - self.vgh = self.Export(self.conn.pins.request('5').adapt_to(VoltageSource( - voltage_out=(2.5, 15)*Volt, - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - self.vsh = self.Export(self.conn.pins.request('20').adapt_to(VoltageSource( - voltage_out=(2.4, 15)*Volt, # inferred from power selection register - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - self.vsl = self.Export(self.conn.pins.request('22').adapt_to(VoltageSource( - voltage_out=(-15, -2.4)*Volt, # inferred from power selection register - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - - self.prevgh = self.Export(self.conn.pins.request('21').adapt_to(VoltageSink( - voltage_limits=(13, 20)*Volt - ))) - self.prevgl = self.Export(self.conn.pins.request('23').adapt_to(VoltageSink( - voltage_limits=(-20, -13)*Volt - ))) - - self.bs = self.Export(self.conn.pins.request('8').adapt_to(din_model)) - self.busy = self.Export(self.conn.pins.request('9').adapt_to(din_model), optional=True) - self.rst = self.Export(self.conn.pins.request('10').adapt_to(din_model)) - self.dc = self.Export(self.conn.pins.request('11').adapt_to(din_model), optional=True) - self.csb = self.Export(self.conn.pins.request('12').adapt_to(din_model)) + self.vss = self.Export(self.conn.pins.request("17").adapt_to(Ground()), [Common]) + self.vdd = self.Export( + self.conn.pins.request("16").adapt_to( + VoltageSink( + voltage_limits=(2.5, 3.7) * Volt, # VCI specs, assumed for all logic + current_draw=(0.001, 2.1) * mAmp, # sleep max to operating typ + ) + ) + ) + self.vddio = self.Export( + self.conn.pins.request("15").adapt_to( + VoltageSink( + voltage_limits=(2.5, 3.7) * Volt, # VCI specs, assumed for all logic + ) + ) + ) + self.vdd1v8 = self.Export( + self.conn.pins.request("18").adapt_to( + VoltageSource( + voltage_out=1.8 * Volt(tol=0), # specs not given + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) + + din_model = DigitalSink.from_supply(self.vss, self.vddio, input_threshold_factor=(0.2, 0.8)) + + self.gdr = self.Export(self.conn.pins.request("2")) + self.rese = self.Export(self.conn.pins.request("3")) + + self.vgl = self.Export( + self.conn.pins.request("4").adapt_to( + VoltageSource( + voltage_out=(-15, -2.5) * Volt, # inferred from power selection register + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) + self.vgh = self.Export( + self.conn.pins.request("5").adapt_to( + VoltageSource( + voltage_out=(2.5, 15) * Volt, current_limits=0 * mAmp(tol=0) # only for external capacitor + ) + ) + ) + self.vsh = self.Export( + self.conn.pins.request("20").adapt_to( + VoltageSource( + voltage_out=(2.4, 15) * Volt, # inferred from power selection register + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) + self.vsl = self.Export( + self.conn.pins.request("22").adapt_to( + VoltageSource( + voltage_out=(-15, -2.4) * Volt, # inferred from power selection register + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) + + self.prevgh = self.Export(self.conn.pins.request("21").adapt_to(VoltageSink(voltage_limits=(13, 20) * Volt))) + self.prevgl = self.Export(self.conn.pins.request("23").adapt_to(VoltageSink(voltage_limits=(-20, -13) * Volt))) + + self.bs = self.Export(self.conn.pins.request("8").adapt_to(din_model)) + self.busy = self.Export(self.conn.pins.request("9").adapt_to(din_model), optional=True) + self.rst = self.Export(self.conn.pins.request("10").adapt_to(din_model)) + self.dc = self.Export(self.conn.pins.request("11").adapt_to(din_model), optional=True) + self.csb = self.Export(self.conn.pins.request("12").adapt_to(din_model)) self.spi = self.Port(SpiPeripheral.empty()) - self.connect(self.spi.sck, self.conn.pins.request('13').adapt_to(din_model)) # SCL - self.connect(self.spi.mosi, self.conn.pins.request('14').adapt_to(din_model)) # SDA + self.connect(self.spi.sck, self.conn.pins.request("13").adapt_to(din_model)) # SCL + self.connect(self.spi.mosi, self.conn.pins.request("14").adapt_to(din_model)) # SDA self.miso_nc = self.Block(DigitalBidirNotConnected()) self.connect(self.spi.miso, self.miso_nc.port) - self.vcom = self.Export(self.conn.pins.request('24').adapt_to(VoltageSource( - voltage_out=(2.4, 20)*Volt, # configurable up to VGH - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) + self.vcom = self.Export( + self.conn.pins.request("24").adapt_to( + VoltageSource( + voltage_out=(2.4, 20) * Volt, # configurable up to VGH + current_limits=0 * mAmp(tol=0), # only for external capacitor + ) + ) + ) class Waveshare_Epd(EInk, GeneratorBlock): @@ -80,6 +105,7 @@ class Waveshare_Epd(EInk, GeneratorBlock): https://www.waveshare.com/wiki/E-Paper_Driver_HAT excluding the "clever" reset circuit """ + def __init__(self) -> None: super().__init__() self.device = self.Block(Waveshare_Epd_Device()) @@ -97,31 +123,46 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() - self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vdd) + self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd + ) self.connect(self.device.vdd, self.device.vddio) - self.vdd1v8_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vdd1v8) + self.vdd1v8_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd1v8 + ) - self.vgl_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vgl) - self.vgh_cap = self.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vgh) - self.vsh_cap = self.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vsh) - self.vsl_cap = self.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vsl) - self.vcom_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.device.vcom) + self.vgl_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vgl + ) + self.vgh_cap = self.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vgh + ) + self.vsh_cap = self.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vsh + ) + self.vsl_cap = self.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vsl + ) + self.vcom_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcom + ) # current limit based on 200mA saturation current of reference inductor - self.boost = self.Block(EInkBoostPowerPath(20*Volt(tol=0), (0, 200)*mAmp, 68*uHenry(tol=0.2), - 4.7*uFarad(tol=0.2), 4.7*uFarad(tol=0.2), 3*Ohm(tol=0.01))) + self.boost = self.Block( + EInkBoostPowerPath( + 20 * Volt(tol=0), + (0, 200) * mAmp, + 68 * uHenry(tol=0.2), + 4.7 * uFarad(tol=0.2), + 4.7 * uFarad(tol=0.2), + 3 * Ohm(tol=0.01), + ) + ) self.connect(self.gnd, self.boost.gnd) self.connect(self.pwr, self.boost.pwr_in) self.connect(self.device.gdr, self.boost.gate) - self.gate_pdr = self.Block(Resistor(10*kOhm(tol=0.05))) + self.gate_pdr = self.Block(Resistor(10 * kOhm(tol=0.05))) self.connect(self.gate_pdr.a, self.boost.gate) self.connect(self.gate_pdr.b.adapt_to(Ground()), self.gnd) self.connect(self.device.rese, self.boost.isense) diff --git a/edg/parts/EnvironmentalSensor_Bme680.py b/edg/parts/EnvironmentalSensor_Bme680.py index f079b69b6..d376c89a4 100644 --- a/edg/parts/EnvironmentalSensor_Bme680.py +++ b/edg/parts/EnvironmentalSensor_Bme680.py @@ -7,46 +7,50 @@ class Bme680_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(1.71, 3.6)*Volt, - current_draw=(0.15*uAmp, 18*mAmp) # typ sleep to peak gas sensor supply - )) - self.vddio = self.Port(VoltageSink( - voltage_limits=(1.2, 3.6)*Volt - )) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(1.71, 3.6) * Volt, + current_draw=(0.15 * uAmp, 18 * mAmp), # typ sleep to peak gas sensor supply + ) + ) + self.vddio = self.Port(VoltageSink(voltage_limits=(1.2, 3.6) * Volt)) self.gnd = self.Port(Ground()) dio_model = DigitalBidir.from_supply( - self.gnd, self.vddio, - voltage_limit_abs=(-0.3*Volt, self.vddio.voltage_limits.upper()+0.3), - input_threshold_factor=(0.2, 0.8) + self.gnd, + self.vddio, + voltage_limit_abs=(-0.3 * Volt, self.vddio.voltage_limits.upper() + 0.3), + input_threshold_factor=(0.2, 0.8), ) self.i2c = self.Port(I2cTarget(dio_model, [0x76])) @override def contents(self) -> None: self.footprint( - 'U', 'Package_LGA:Bosch_LGA-8_3x3mm_P0.8mm_ClockwisePinNumbering', + "U", + "Package_LGA:Bosch_LGA-8_3x3mm_P0.8mm_ClockwisePinNumbering", { - '1': self.gnd, - '2': self.vddio, # different in SPI mode - '3': self.i2c.sda, - '4': self.i2c.scl, - '5': self.gnd, # TODO address - '6': self.vddio, - '7': self.gnd, - '8': self.vdd, + "1": self.gnd, + "2": self.vddio, # different in SPI mode + "3": self.i2c.sda, + "4": self.i2c.scl, + "5": self.gnd, # TODO address + "6": self.vddio, + "7": self.gnd, + "8": self.vdd, }, - mfr='Bosch Sensortec', part='BME680', - datasheet='https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme680-ds001.pdf' + mfr="Bosch Sensortec", + part="BME680", + datasheet="https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme680-ds001.pdf", ) - self.assign(self.lcsc_part, 'C125972') + self.assign(self.lcsc_part, "C125972") self.assign(self.actual_basic_part, False) class Bme680(TemperatureSensor, HumiditySensor, PressureSensor, GasSensor, DefaultExportBlock): """Gas (indoor air quality), pressure, temperature, and humidity sensor. Humidity accuracy /-3% RH, pressure noise 0.12 Pa, temperature accuracy +/-0.5 C @ 25C""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Bme680_Device()) @@ -58,5 +62,5 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() # capacitors from shuttle board example - self.vdd_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) - self.vddio_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vddio) + self.vdd_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + self.vddio_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vddio) diff --git a/edg/parts/EnvironmentalSensor_Sensirion.py b/edg/parts/EnvironmentalSensor_Sensirion.py index 89f1b442b..8dca1413b 100644 --- a/edg/parts/EnvironmentalSensor_Sensirion.py +++ b/edg/parts/EnvironmentalSensor_Sensirion.py @@ -7,38 +7,37 @@ class Shtc3_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(1.62, 3.6)*Volt, - current_draw=(0.3, 900)*uAmp # typ sleep to max meas - )) + self.vdd = self.Port( + VoltageSink(voltage_limits=(1.62, 3.6) * Volt, current_draw=(0.3, 900) * uAmp) # typ sleep to max meas + ) self.vss = self.Port(Ground()) - dio_model = DigitalBidir.from_supply( - self.vss, self.vdd, - input_threshold_factor=(0.42, 0.7) - ) + dio_model = DigitalBidir.from_supply(self.vss, self.vdd, input_threshold_factor=(0.42, 0.7)) self.i2c = self.Port(I2cTarget(dio_model, [0x70])) @override def contents(self) -> None: self.footprint( - 'U', 'Sensor_Humidity:Sensirion_DFN-4-1EP_2x2mm_P1mm_EP0.7x1.6mm', + "U", + "Sensor_Humidity:Sensirion_DFN-4-1EP_2x2mm_P1mm_EP0.7x1.6mm", { - '1': self.vdd, - '2': self.i2c.scl, - '3': self.i2c.sda, - '4': self.vss, - '5': self.vss, # EP, recommended to be soldered for mechanical reasons (section 4) + "1": self.vdd, + "2": self.i2c.scl, + "3": self.i2c.sda, + "4": self.vss, + "5": self.vss, # EP, recommended to be soldered for mechanical reasons (section 4) }, - mfr='Sensirion AG', part='SHTC3', - datasheet='https://www.sensirion.com/media/documents/643F9C8E/63A5A436/Datasheet_SHTC3.pdf' + mfr="Sensirion AG", + part="SHTC3", + datasheet="https://www.sensirion.com/media/documents/643F9C8E/63A5A436/Datasheet_SHTC3.pdf", ) - self.assign(self.lcsc_part, 'C194656') + self.assign(self.lcsc_part, "C194656") self.assign(self.actual_basic_part, False) class Shtc3(TemperatureSensor, HumiditySensor, Block): """Humidity and temperature sensor with +/-2% RH and +/-0.2C""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Shtc3_Device()) @@ -49,4 +48,4 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() # capacitors from datasheet - self.vdd_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + self.vdd_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) diff --git a/edg/parts/EnvironmentalSensor_Ti.py b/edg/parts/EnvironmentalSensor_Ti.py index 6b375182c..f57f7d585 100644 --- a/edg/parts/EnvironmentalSensor_Ti.py +++ b/edg/parts/EnvironmentalSensor_Ti.py @@ -8,39 +8,38 @@ class Hdc1080_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, - current_draw=(0.1, 300)*uAmp # sleep to startup average - )) - - dio_model = DigitalBidir.from_supply( - self.gnd, self.vdd, - input_threshold_factor=(0.3, 0.7) + self.vdd = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(0.1, 300) * uAmp) # sleep to startup average ) + + dio_model = DigitalBidir.from_supply(self.gnd, self.vdd, input_threshold_factor=(0.3, 0.7)) self.i2c = self.Port(I2cTarget(dio_model, [0x40])) @override def contents(self) -> None: super().contents() self.footprint( - 'U', 'Package_SON:WSON-6-1EP_3x3mm_P0.95mm', + "U", + "Package_SON:WSON-6-1EP_3x3mm_P0.95mm", { - '1': self.i2c.sda, - '2': self.gnd, + "1": self.i2c.sda, + "2": self.gnd, # 3, 4 are NC - '5': self.vdd, - '6': self.i2c.scl, + "5": self.vdd, + "6": self.i2c.scl, # EP explicitly should be left floating }, - mfr='Texas Instruments', part='HDC1080', - datasheet='https://www.ti.com/lit/ds/symlink/hdc1080.pdf' + mfr="Texas Instruments", + part="HDC1080", + datasheet="https://www.ti.com/lit/ds/symlink/hdc1080.pdf", ) - self.assign(self.lcsc_part, 'C82227') + self.assign(self.lcsc_part, "C82227") self.assign(self.actual_basic_part, False) class Hdc1080(TemperatureSensor, HumiditySensor, Block): """Temperature and humidity sensor with +/- 0.2C and +/- 2% RH typical accuracy""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Hdc1080_Device()) @@ -52,22 +51,18 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() # X7R capacitor recommended - self.vdd_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + self.vdd_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) class Tmp1075n_Device(InternalSubcircuit, FootprintBlock, JlcPart, GeneratorBlock): def __init__(self, addr_lsb: IntLike) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vdd = self.Port(VoltageSink( - voltage_limits=(1.62, 3.6)*Volt, - current_draw=(0.5, 85)*uAmp # shutdown to serial active - )) - - dio_model = DigitalBidir.from_supply( - self.gnd, self.vdd, - input_threshold_factor=(0.3, 0.7) + self.vdd = self.Port( + VoltageSink(voltage_limits=(1.62, 3.6) * Volt, current_draw=(0.5, 85) * uAmp) # shutdown to serial active ) + + dio_model = DigitalBidir.from_supply(self.gnd, self.vdd, input_threshold_factor=(0.3, 0.7)) self.i2c = self.Port(I2cTarget(dio_model, addresses=ArrayIntExpr())) self.alert = self.Port(DigitalSource.low_from_supply(self.gnd), optional=True) @@ -83,24 +78,27 @@ def generate(self) -> None: self.assign(self.i2c.addresses, [0x48 | addr_lsb]) self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-563', + "U", + "Package_TO_SOT_SMD:SOT-563", { - '1': self.i2c.scl, - '2': self.gnd, - '3': self.alert, - '4': self.vdd if addr_lsb & 1 else self.gnd, # A0 - '5': self.vdd, - '6': self.i2c.sda, + "1": self.i2c.scl, + "2": self.gnd, + "3": self.alert, + "4": self.vdd if addr_lsb & 1 else self.gnd, # A0 + "5": self.vdd, + "6": self.i2c.sda, }, - mfr='Texas Instruments', part='TMP1075N', - datasheet='https://www.ti.com/lit/ds/symlink/tmp1075.pdf' + mfr="Texas Instruments", + part="TMP1075N", + datasheet="https://www.ti.com/lit/ds/symlink/tmp1075.pdf", ) - self.assign(self.lcsc_part, 'C3663690') + self.assign(self.lcsc_part, "C3663690") self.assign(self.actual_basic_part, False) class Tmp1075n(TemperatureSensor, Block): """Temperature sensor with 0.25C typical accuracy""" + def __init__(self, addr_lsb: IntLike = 0): super().__init__() self.ic = self.Block(Tmp1075n_Device(addr_lsb)) @@ -113,4 +111,4 @@ def __init__(self, addr_lsb: IntLike = 0): @override def contents(self) -> None: super().contents() - self.vdd_cap = self.Block(DecouplingCapacitor(0.01*uFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + self.vdd_cap = self.Block(DecouplingCapacitor(0.01 * uFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) diff --git a/edg/parts/FanConnector.py b/edg/parts/FanConnector.py index cef800704..15f05f25d 100644 --- a/edg/parts/FanConnector.py +++ b/edg/parts/FanConnector.py @@ -6,58 +6,69 @@ @abstract_block_default(lambda: CpuFan3Pin) class CpuFanConnector(Connector, Block): """Abstract block for a 3-pin CPU fan connector.""" + def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground(), [Common]) - self.pwr = self.Port(VoltageSink( - voltage_limits=12*Volt(tol=0.05), - current_draw=(0, 2.2)*Amp, # section 2.1.2: 2.2A max up to 1s during startup, steady-state 1.5A - ), [Power]) + self.pwr = self.Port( + VoltageSink( + voltage_limits=12 * Volt(tol=0.05), + current_draw=(0, 2.2) * Amp, # section 2.1.2: 2.2A max up to 1s during startup, steady-state 1.5A + ), + [Power], + ) self.sense = self.Port(DigitalSource.low_from_supply(self.gnd)) # tolerant up to 12v @abstract_block_default(lambda: CpuFan4Pin) class CpuFanPwmControl(BlockInterfaceMixin[CpuFanConnector]): """Mixin that adds an PWM control pin (open-collector input) to a CpuFanConnector.""" + def __init__(self) -> None: super().__init__() - self.control = self.Port(DigitalBidir( - voltage_limits=(0, 5.25)*Volt, - voltage_out=(0, 5.25)*Volt, - pullup_capable=True, - input_thresholds=(0.8, 0.8)*Volt, # only low threshold defined - )) # internally pulled up, source drives with open-drain + self.control = self.Port( + DigitalBidir( + voltage_limits=(0, 5.25) * Volt, + voltage_out=(0, 5.25) * Volt, + pullup_capable=True, + input_thresholds=(0.8, 0.8) * Volt, # only low threshold defined + ) + ) # internally pulled up, source drives with open-drain class CpuFan3Pin(CpuFanConnector, FootprintBlock): """3-pin fan controller""" + @override def contents(self) -> None: super().contents() self.footprint( - 'J', 'Connector:FanPinHeader_1x03_P2.54mm_Vertical', + "J", + "Connector:FanPinHeader_1x03_P2.54mm_Vertical", { - '1': self.gnd, - '2': self.pwr, - '3': self.sense, + "1": self.gnd, + "2": self.pwr, + "3": self.sense, }, - part='3-pin CPU fan connector', + part="3-pin CPU fan connector", ) class CpuFan4Pin(CpuFanConnector, CpuFanPwmControl, FootprintBlock): """3-pin fan controller""" + @override def contents(self) -> None: super().contents() self.footprint( - 'J', 'Connector:FanPinHeader_1x04_P2.54mm_Vertical', + "J", + "Connector:FanPinHeader_1x04_P2.54mm_Vertical", { - '1': self.gnd, - '2': self.pwr, - '3': self.sense, - '4': self.control, + "1": self.gnd, + "2": self.pwr, + "3": self.sense, + "4": self.control, }, - part='4-pin CPU fan connector', - datasheet='https://www.intel.com/content/dam/support/us/en/documents/intel-nuc/intel-4wire-pwm-fans-specs.pdf' + part="4-pin CPU fan connector", + datasheet="https://www.intel.com/content/dam/support/us/en/documents/intel-nuc/intel-4wire-pwm-fans-specs.pdf", ) diff --git a/edg/parts/Fpga_Ice40up.py b/edg/parts/Fpga_Ice40up.py index 6ae06b177..5cd1a9399 100644 --- a/edg/parts/Fpga_Ice40up.py +++ b/edg/parts/Fpga_Ice40up.py @@ -8,319 +8,332 @@ class Ice40TargetHeader(ProgrammingConnector, FootprintBlock): - """Custom programming header for iCE40 loosely based on the SWD pinning""" - def __init__(self) -> None: - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) # in practice this can power the target - self.gnd = self.Port(Ground.empty(), [Common]) # TODO pin at 0v - self.spi = self.Port(SpiController.empty()) - self.cs = self.Port(DigitalSource.empty()) - self.reset = self.Port(DigitalSource.empty()) - - @override - def contents(self) -> None: - super().contents() - self.conn = self.Block(PinHeader127DualShrouded(10)) - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - self.connect(self.gnd, self.conn.pins.request('3').adapt_to(Ground()), - self.conn.pins.request('5').adapt_to(Ground()), - self.conn.pins.request('9').adapt_to(Ground())) - self.connect(self.cs, self.conn.pins.request('2').adapt_to(DigitalSource())) # swd: swdio - self.connect(self.spi.sck, self.conn.pins.request('4').adapt_to(DigitalSource())) # swd: swclk - self.connect(self.spi.mosi, self.conn.pins.request('6').adapt_to(DigitalSource())) # swd: swo - self.connect(self.spi.miso, self.conn.pins.request('8').adapt_to(DigitalSink())) # swd: NC or jtag: tdi - self.connect(self.reset, self.conn.pins.request('10').adapt_to(DigitalSource())) + """Custom programming header for iCE40 loosely based on the SWD pinning""" + + def __init__(self) -> None: + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), [Power]) # in practice this can power the target + self.gnd = self.Port(Ground.empty(), [Common]) # TODO pin at 0v + self.spi = self.Port(SpiController.empty()) + self.cs = self.Port(DigitalSource.empty()) + self.reset = self.Port(DigitalSource.empty()) + + @override + def contents(self) -> None: + super().contents() + self.conn = self.Block(PinHeader127DualShrouded(10)) + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + self.connect( + self.gnd, + self.conn.pins.request("3").adapt_to(Ground()), + self.conn.pins.request("5").adapt_to(Ground()), + self.conn.pins.request("9").adapt_to(Ground()), + ) + self.connect(self.cs, self.conn.pins.request("2").adapt_to(DigitalSource())) # swd: swdio + self.connect(self.spi.sck, self.conn.pins.request("4").adapt_to(DigitalSource())) # swd: swclk + self.connect(self.spi.mosi, self.conn.pins.request("6").adapt_to(DigitalSource())) # swd: swo + self.connect(self.spi.miso, self.conn.pins.request("8").adapt_to(DigitalSink())) # swd: NC or jtag: tdi + self.connect(self.reset, self.conn.pins.request("10").adapt_to(DigitalSource())) @abstract_block class Ice40up_Device(BaseIoControllerPinmapGenerator, InternalSubcircuit, GeneratorBlock, JlcPart, FootprintBlock): - """Base class for iCE40 UltraPlus FPGAs, 2.8k-5.2k logic cells.""" - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) - RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name - PACKAGE: str # package name for footprint(...) - PART: str # part name for footprint(...) - JLC_PART: str # part number for lcsc_part - - BITSTREAM_BITS: int = 0 - - @staticmethod - def make_dio_model(gnd: Ground, vccio: VoltageSink) -> DigitalBidir: - return DigitalBidir.from_supply( - gnd, vccio, - voltage_limit_tolerance=(-0.3, 0.2) * Volt, # table 4.13 - current_limits=(-8, 8)*mAmp, # for LVCMOS 3.3 (least restrictive) - input_threshold_abs=(0.8, 2.0), # most restrictive, for LVCMOS 3.3 - pullup_capable=True, pulldown_capable=False, - ) - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.gnd = self.Port(Ground(), [Common]) - self.vccio_1 = self.Port(VoltageSink( - voltage_limits=(1.71, 3.46)*Volt, # table 4.2 - current_draw=(0.0005, 9)*mAmp + self.io_current_draw.upper() # table 4.6, static to startup peak; no max given - )) # TODO IO currents should be allocated per-bank - - vccio_model = VoltageSink( - voltage_limits=(1.71, 3.46)*Volt, # table 4.2 - current_draw=(0.0005, 2) * mAmp # table 4.6, static to startup peak - ) - self.vccio_0 = self.Port(vccio_model) - self.vccio_2 = self.Port(vccio_model) - - self.vcc = self.Port(VoltageSink( # core supply voltage - voltage_limits=(1.14, 1.26) * Volt, # table 4.2 - current_draw=(0.075, 12) * mAmp # table 4.6, static to startup peak; no max given - )) - self.vpp_2v5 = self.Port(VoltageSink( # NVCM operating voltage, excluding programming mode - voltage_limits=(2.3, 3.45) * Volt, # table 4.2, for master/slave/NVCM config - )) - self.vcc_pll = self.Port(VoltageSink( # PLL supply voltage - voltage_limits=(1.14, 1.26) * Volt, # table 4.2 - )) - # note, section 4.5 recommended power sequence: Vcc, Vcc_pll -> Spi_VccIO1 -> Vpp_2v5 - # VccIO0/2 can be applied anytime after Vcc, Vcc_pll - self._pio1_model = self.make_dio_model(self.gnd, self.vccio_1) - self._dpio1_model = self._pio1_model - - self.creset_b = self.Port(DigitalSink.from_bidir(self._pio1_model)) # no internal PUR, must be driven (or 10k pulled up) - self.cdone = self.Port(DigitalSource.from_bidir(self._pio1_model), optional=True) # dedicated on SG48, shared on UWG30 - - # TODO requirements on SPI device frequency - # TODO this is really bidirectional, so this could be either separate ports or split ports - self.spi_config = self.Port(SpiController(self._dpio1_model)) - self.spi_config_cs = self.Port(self._dpio1_model) - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: # names consistent with pinout spreadsheet - return VariantPinRemapper({ - 'VCCPLL': self.vcc_pll, - 'VCC': self.vcc, - 'SPI_Vccio1': self.vccio_1, - 'VCCIO_0': self.vccio_0, - 'VCCIO_2': self.vccio_2, - 'VPP_2V5': self.vpp_2v5, - 'GND': self.gnd, - - 'CRESET_B': self.creset_b, - 'CDONE': self.cdone, - - 'IOB_32a_SPI_SO': self.spi_config.mosi, - 'IOB_33b_SPI_SI': self.spi_config.miso, - 'IOB_34a_SPI_SCK': self.spi_config.sck, - 'IOB_35b_SPI_SS': self.spi_config_cs, - }).remap(self.SYSTEM_PIN_REMAP) - - @override - def _io_pinmap(self) -> PinMapUtil: - pio0_model = self.make_dio_model(self.gnd, self.vccio_0) - dpio0_model = pio0_model # differential capability currently not modeled - pio1_model = self._pio1_model - dpio1_model = self._dpio1_model - pio2_model = self.make_dio_model(self.gnd, self.vccio_2) - dpio2_model = pio2_model - - - # hard macros, not tied to any particular pin - i2c_model = I2cController(DigitalBidir.empty()) # user I2C, table 4.7 - spi_model = SpiController(DigitalBidir.empty(), (0, 45) * MHertz) # user SPI, table 4.10 - - return PinMapUtil([ # names consistent with pinout spreadsheet - PinResource('IOB_0a', {'IOB_0a': pio2_model}), - PinResource('IOB_2a', {'IOB_2a': dpio2_model}), - PinResource('IOB_3b_G6', {'IOB_3b_G6': dpio2_model}), - PinResource('IOB_4a', {'IOB_4a': dpio2_model}), - PinResource('IOB_5b', {'IOB_5b': dpio2_model}), - PinResource('IOB_6a', {'IOB_6a': pio2_model}), - PinResource('IOB_8a', {'IOB_8a': dpio2_model}), - PinResource('IOB_9b', {'IOB_9b': dpio2_model}), - - PinResource('IOB_10a', {'IOB_10a': dpio1_model}), - PinResource('IOB_11b_G5', {'IOB_11b_G5': dpio1_model}), - PinResource('IOB_12a_G4', {'IOB_12a_G4': dpio1_model}), # shared with DONE on UWG30 - PinResource('IOB_13b', {'IOB_13b': dpio1_model}), - PinResource('IOB_16a', {'IOB_16a': pio1_model}), - PinResource('IOB_18a', {'IOB_18a': pio1_model}), - PinResource('IOB_20a', {'IOB_20a': pio1_model}), - PinResource('IOB_22a', {'IOB_22a': dpio1_model}), - PinResource('IOB_23b', {'IOB_23b': dpio1_model}), - PinResource('IOB_24a', {'IOB_24a': dpio1_model}), - PinResource('IOB_25b_G3', {'IOB_25b_G3': dpio1_model}), - PinResource('IOB_29b', {'IOB_29b': pio1_model}), - PinResource('IOB_31b', {'IOB_31b': pio1_model}), - - PinResource('IOB_36b', {'IOB_36b': dpio0_model}), - PinResource('IOB_37a', {'IOB_37a': dpio0_model}), - PinResource('IOB_38b', {'IOB_38b': dpio0_model}), - PinResource('IOB_39a', {'IOB_39a': dpio0_model}), - PinResource('IOB_41a', {'IOB_41a': pio0_model}), - PinResource('IOB_42b', {'IOB_42b': dpio0_model}), - PinResource('IOB_43a', {'IOB_43a': dpio0_model}), - PinResource('IOB_44b', {'IOB_44b': dpio0_model}), - PinResource('IOB_45a_G1', {'IOB_45a_G1': dpio0_model}), - PinResource('IOB_46b_G0', {'IOB_46b_G0': dpio0_model}), - PinResource('IOB_47a', {'IOB_47a': pio0_model}), - PinResource('IOB_48b', {'IOB_48b': dpio0_model}), - PinResource('IOB_49a', {'IOB_49a': dpio0_model}), - PinResource('IOB_50b', {'IOB_50b': dpio0_model}), - PinResource('IOB_51a', {'IOB_51a': dpio0_model}), - - # RGB2/1/0 skipped since they're open-drain only and constant-current analog drive - - # hard macros I2C and SPI - PeripheralAnyResource('I2C1', i2c_model), - PeripheralAnyResource('I2C1', i2c_model), - PeripheralAnyResource('SPI1', spi_model), - PeripheralAnyResource('SPI2', spi_model), - ]).remap_pins(self.RESOURCE_PIN_REMAP) - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', self.PACKAGE, - self._make_pinning(), - mfr='Lattice Semiconductor Corporation', part=self.PART, - datasheet='https://www.latticesemi.com/-/media/LatticeSemi/Documents/DataSheets/iCE/iCE40-UltraPlus-Family-Data-Sheet.ashx' - ) - self.assign(self.lcsc_part, self.JLC_PART) - self.assign(self.actual_basic_part, False) + """Base class for iCE40 UltraPlus FPGAs, 2.8k-5.2k logic cells.""" + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) + RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name + PACKAGE: str # package name for footprint(...) + PART: str # part name for footprint(...) + JLC_PART: str # part number for lcsc_part + + BITSTREAM_BITS: int = 0 + + @staticmethod + def make_dio_model(gnd: Ground, vccio: VoltageSink) -> DigitalBidir: + return DigitalBidir.from_supply( + gnd, + vccio, + voltage_limit_tolerance=(-0.3, 0.2) * Volt, # table 4.13 + current_limits=(-8, 8) * mAmp, # for LVCMOS 3.3 (least restrictive) + input_threshold_abs=(0.8, 2.0), # most restrictive, for LVCMOS 3.3 + pullup_capable=True, + pulldown_capable=False, + ) + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.gnd = self.Port(Ground(), [Common]) + self.vccio_1 = self.Port( + VoltageSink( + voltage_limits=(1.71, 3.46) * Volt, # table 4.2 + current_draw=(0.0005, 9) * mAmp + + self.io_current_draw.upper(), # table 4.6, static to startup peak; no max given + ) + ) # TODO IO currents should be allocated per-bank + + vccio_model = VoltageSink( + voltage_limits=(1.71, 3.46) * Volt, # table 4.2 + current_draw=(0.0005, 2) * mAmp, # table 4.6, static to startup peak + ) + self.vccio_0 = self.Port(vccio_model) + self.vccio_2 = self.Port(vccio_model) + + self.vcc = self.Port( + VoltageSink( # core supply voltage + voltage_limits=(1.14, 1.26) * Volt, # table 4.2 + current_draw=(0.075, 12) * mAmp, # table 4.6, static to startup peak; no max given + ) + ) + self.vpp_2v5 = self.Port( + VoltageSink( # NVCM operating voltage, excluding programming mode + voltage_limits=(2.3, 3.45) * Volt, # table 4.2, for master/slave/NVCM config + ) + ) + self.vcc_pll = self.Port( + VoltageSink( # PLL supply voltage + voltage_limits=(1.14, 1.26) * Volt, # table 4.2 + ) + ) + # note, section 4.5 recommended power sequence: Vcc, Vcc_pll -> Spi_VccIO1 -> Vpp_2v5 + # VccIO0/2 can be applied anytime after Vcc, Vcc_pll + self._pio1_model = self.make_dio_model(self.gnd, self.vccio_1) + self._dpio1_model = self._pio1_model + + self.creset_b = self.Port( + DigitalSink.from_bidir(self._pio1_model) + ) # no internal PUR, must be driven (or 10k pulled up) + self.cdone = self.Port( + DigitalSource.from_bidir(self._pio1_model), optional=True + ) # dedicated on SG48, shared on UWG30 + + # TODO requirements on SPI device frequency + # TODO this is really bidirectional, so this could be either separate ports or split ports + self.spi_config = self.Port(SpiController(self._dpio1_model)) + self.spi_config_cs = self.Port(self._dpio1_model) + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: # names consistent with pinout spreadsheet + return VariantPinRemapper( + { + "VCCPLL": self.vcc_pll, + "VCC": self.vcc, + "SPI_Vccio1": self.vccio_1, + "VCCIO_0": self.vccio_0, + "VCCIO_2": self.vccio_2, + "VPP_2V5": self.vpp_2v5, + "GND": self.gnd, + "CRESET_B": self.creset_b, + "CDONE": self.cdone, + "IOB_32a_SPI_SO": self.spi_config.mosi, + "IOB_33b_SPI_SI": self.spi_config.miso, + "IOB_34a_SPI_SCK": self.spi_config.sck, + "IOB_35b_SPI_SS": self.spi_config_cs, + } + ).remap(self.SYSTEM_PIN_REMAP) + + @override + def _io_pinmap(self) -> PinMapUtil: + pio0_model = self.make_dio_model(self.gnd, self.vccio_0) + dpio0_model = pio0_model # differential capability currently not modeled + pio1_model = self._pio1_model + dpio1_model = self._dpio1_model + pio2_model = self.make_dio_model(self.gnd, self.vccio_2) + dpio2_model = pio2_model + + # hard macros, not tied to any particular pin + i2c_model = I2cController(DigitalBidir.empty()) # user I2C, table 4.7 + spi_model = SpiController(DigitalBidir.empty(), (0, 45) * MHertz) # user SPI, table 4.10 + + return PinMapUtil( + [ # names consistent with pinout spreadsheet + PinResource("IOB_0a", {"IOB_0a": pio2_model}), + PinResource("IOB_2a", {"IOB_2a": dpio2_model}), + PinResource("IOB_3b_G6", {"IOB_3b_G6": dpio2_model}), + PinResource("IOB_4a", {"IOB_4a": dpio2_model}), + PinResource("IOB_5b", {"IOB_5b": dpio2_model}), + PinResource("IOB_6a", {"IOB_6a": pio2_model}), + PinResource("IOB_8a", {"IOB_8a": dpio2_model}), + PinResource("IOB_9b", {"IOB_9b": dpio2_model}), + PinResource("IOB_10a", {"IOB_10a": dpio1_model}), + PinResource("IOB_11b_G5", {"IOB_11b_G5": dpio1_model}), + PinResource("IOB_12a_G4", {"IOB_12a_G4": dpio1_model}), # shared with DONE on UWG30 + PinResource("IOB_13b", {"IOB_13b": dpio1_model}), + PinResource("IOB_16a", {"IOB_16a": pio1_model}), + PinResource("IOB_18a", {"IOB_18a": pio1_model}), + PinResource("IOB_20a", {"IOB_20a": pio1_model}), + PinResource("IOB_22a", {"IOB_22a": dpio1_model}), + PinResource("IOB_23b", {"IOB_23b": dpio1_model}), + PinResource("IOB_24a", {"IOB_24a": dpio1_model}), + PinResource("IOB_25b_G3", {"IOB_25b_G3": dpio1_model}), + PinResource("IOB_29b", {"IOB_29b": pio1_model}), + PinResource("IOB_31b", {"IOB_31b": pio1_model}), + PinResource("IOB_36b", {"IOB_36b": dpio0_model}), + PinResource("IOB_37a", {"IOB_37a": dpio0_model}), + PinResource("IOB_38b", {"IOB_38b": dpio0_model}), + PinResource("IOB_39a", {"IOB_39a": dpio0_model}), + PinResource("IOB_41a", {"IOB_41a": pio0_model}), + PinResource("IOB_42b", {"IOB_42b": dpio0_model}), + PinResource("IOB_43a", {"IOB_43a": dpio0_model}), + PinResource("IOB_44b", {"IOB_44b": dpio0_model}), + PinResource("IOB_45a_G1", {"IOB_45a_G1": dpio0_model}), + PinResource("IOB_46b_G0", {"IOB_46b_G0": dpio0_model}), + PinResource("IOB_47a", {"IOB_47a": pio0_model}), + PinResource("IOB_48b", {"IOB_48b": dpio0_model}), + PinResource("IOB_49a", {"IOB_49a": dpio0_model}), + PinResource("IOB_50b", {"IOB_50b": dpio0_model}), + PinResource("IOB_51a", {"IOB_51a": dpio0_model}), + # RGB2/1/0 skipped since they're open-drain only and constant-current analog drive + # hard macros I2C and SPI + PeripheralAnyResource("I2C1", i2c_model), + PeripheralAnyResource("I2C1", i2c_model), + PeripheralAnyResource("SPI1", spi_model), + PeripheralAnyResource("SPI2", spi_model), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + self.PACKAGE, + self._make_pinning(), + mfr="Lattice Semiconductor Corporation", + part=self.PART, + datasheet="https://www.latticesemi.com/-/media/LatticeSemi/Documents/DataSheets/iCE/iCE40-UltraPlus-Family-Data-Sheet.ashx", + ) + self.assign(self.lcsc_part, self.JLC_PART) + self.assign(self.actual_basic_part, False) class Ice40up5k_Sg48_Device(Ice40up_Device): - SYSTEM_PIN_REMAP = { - 'VCCPLL': '29', - 'VCC': ['5', '30'], - 'SPI_Vccio1': '22', - 'VCCIO_0': '33', - 'VCCIO_2': '1', - 'VPP_2V5': '24', - 'GND': '49', # "Paddle" - - 'CRESET_B': '8', - 'CDONE': '7', - - 'IOB_32a_SPI_SO': '14', - 'IOB_33b_SPI_SI': '17', - 'IOB_34a_SPI_SCK': '15', - 'IOB_35b_SPI_SS': '16', - } - - RESOURCE_PIN_REMAP = { - 'IOB_0a': '46', - 'IOB_2a': '47', - 'IOB_3b_G6': '44', - 'IOB_4a': '48', - 'IOB_5b': '45', - 'IOB_6a': '2', - 'IOB_8a': '4', - 'IOB_9b': '3', - 'IOB_13b': '6', - 'IOB_16a': '9', - 'IOB_18a': '10', - 'IOB_20a': '11', - 'IOB_22a': '12', - 'IOB_23b': '21', - 'IOB_24a': '13', - 'IOB_25b_G3': '20', - 'IOB_29b': '19', - 'IOB_31b': '18', - 'IOB_36b': '25', - 'IOB_37a': '23', - 'IOB_38b': '27', - 'IOB_39a': '26', - 'IOB_41a': '28', - 'IOB_42b': '31', - 'IOB_43a': '32', - 'IOB_44b': '34', - 'IOB_45a_G1': '37', - 'IOB_46b_G0': '35', - 'IOB_48b': '36', - 'IOB_49a': '43', - 'IOB_50b': '38', - 'IOB_51a': '42', - } - - PACKAGE = 'Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm' - PART = 'ICE40UP5K-SG48' - JLC_PART = 'C2678152' - - BITSTREAM_BITS = 833288 + SYSTEM_PIN_REMAP = { + "VCCPLL": "29", + "VCC": ["5", "30"], + "SPI_Vccio1": "22", + "VCCIO_0": "33", + "VCCIO_2": "1", + "VPP_2V5": "24", + "GND": "49", # "Paddle" + "CRESET_B": "8", + "CDONE": "7", + "IOB_32a_SPI_SO": "14", + "IOB_33b_SPI_SI": "17", + "IOB_34a_SPI_SCK": "15", + "IOB_35b_SPI_SS": "16", + } + + RESOURCE_PIN_REMAP = { + "IOB_0a": "46", + "IOB_2a": "47", + "IOB_3b_G6": "44", + "IOB_4a": "48", + "IOB_5b": "45", + "IOB_6a": "2", + "IOB_8a": "4", + "IOB_9b": "3", + "IOB_13b": "6", + "IOB_16a": "9", + "IOB_18a": "10", + "IOB_20a": "11", + "IOB_22a": "12", + "IOB_23b": "21", + "IOB_24a": "13", + "IOB_25b_G3": "20", + "IOB_29b": "19", + "IOB_31b": "18", + "IOB_36b": "25", + "IOB_37a": "23", + "IOB_38b": "27", + "IOB_39a": "26", + "IOB_41a": "28", + "IOB_42b": "31", + "IOB_43a": "32", + "IOB_44b": "34", + "IOB_45a_G1": "37", + "IOB_46b_G0": "35", + "IOB_48b": "36", + "IOB_49a": "43", + "IOB_50b": "38", + "IOB_51a": "42", + } + + PACKAGE = "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm" + PART = "ICE40UP5K-SG48" + JLC_PART = "C2678152" + + BITSTREAM_BITS = 833288 @abstract_block class Ice40up(Fpga, IoController): - """Application circuit for the iCE40UP series FPGAs, pre-baked for 'common' applications - (3.3v supply with 1.2v core not shared, external FLASH programming, no NVCM programming). - - TODO: generator support for CRAM (volatile) programming mode, diode 2v5 NVCM supply. - """ - DEVICE: Type[Ice40up_Device] = Ice40up_Device - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.cdone = self.Port(DigitalSource.empty(), optional=True) - - @override - def contents(self) -> None: - super().contents() - - # schematics don't seem to be available for the official reference designs, - # so the decoupling caps are kind of arbitrary (except the PLL) - # in theory, there are supply sequencing requirements, but designs like UPduino - # seem to work fine without sequencing circuitry - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.ic = imp.Block(self.DEVICE(pin_assigns=self.pin_assigns)) - self.connect(self.pwr, self.ic.vccio_1, self.ic.vccio_0, self.ic.vccio_2, self.ic.vpp_2v5) - self._export_ios_from(self.ic) - self.assign(self.actual_pin_assigns, self.ic.actual_pin_assigns) - self.connect(self.ic.cdone, self.cdone) - - self.vcc_reg = imp.Block(LinearRegulator((1.14, 1.26)*Volt)) - self.reset_pu = imp.Block(PullupResistor(10*kOhm(tol=0.05))).connected(io=self.ic.creset_b) - - self.mem = imp.Block(SpiMemory(Range.from_lower(self.ic.BITSTREAM_BITS))) - - self.prog = imp.Block(Ice40TargetHeader()) - self.connect(self.prog.reset, self.ic.creset_b) - - # this defaults to flash programming, but to use CRAM programming you can swap the - # SDI/SDO pins on the debug probe and disconnect the CS line - self.spi_merge = self.Block(MergedSpiController()).connected_from(self.ic.spi_config, self.prog.spi) - self.connect(self.spi_merge.out, self.mem.spi) - self.connect(self.ic.spi_config_cs, self.prog.cs) - (self.cs_jmp, ), _ = self.chain(self.ic.spi_config_cs, self.Block(DigitalJumper()), self.mem.cs) - # SPI_SS_B is sampled on boot to determine boot config, needs to be high for PROM config - self.mem_pu = imp.Block(PullupResistor(10*kOhm(tol=0.05))).connected(io=self.mem.cs) - - self.vio_cap0 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.vio_cap1 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.vio_cap2 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.vpp_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - - with self.implicit_connect( # Vcc (core) power domain - ImplicitConnect(self.vcc_reg.pwr_out, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.connect(self.vcc_reg.pwr_out, self.ic.vcc) - self.pll_res = imp.Block(SeriesPowerResistor(100*Ohm(tol=0.05))) - - self.vcc_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - - with self.implicit_connect( # PLL power domain, section 2 of the iCE40 Hardware Checklist - ImplicitConnect(self.pll_res.pwr_out, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.connect(self.pll_res.pwr_out, self.ic.vcc_pll) - - self.pll_lf = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) - self.pll_hf = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + """Application circuit for the iCE40UP series FPGAs, pre-baked for 'common' applications + (3.3v supply with 1.2v core not shared, external FLASH programming, no NVCM programming). + + TODO: generator support for CRAM (volatile) programming mode, diode 2v5 NVCM supply. + """ + + DEVICE: Type[Ice40up_Device] = Ice40up_Device + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.cdone = self.Port(DigitalSource.empty(), optional=True) + + @override + def contents(self) -> None: + super().contents() + + # schematics don't seem to be available for the official reference designs, + # so the decoupling caps are kind of arbitrary (except the PLL) + # in theory, there are supply sequencing requirements, but designs like UPduino + # seem to work fine without sequencing circuitry + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.ic = imp.Block(self.DEVICE(pin_assigns=self.pin_assigns)) + self.connect(self.pwr, self.ic.vccio_1, self.ic.vccio_0, self.ic.vccio_2, self.ic.vpp_2v5) + self._export_ios_from(self.ic) + self.assign(self.actual_pin_assigns, self.ic.actual_pin_assigns) + self.connect(self.ic.cdone, self.cdone) + + self.vcc_reg = imp.Block(LinearRegulator((1.14, 1.26) * Volt)) + self.reset_pu = imp.Block(PullupResistor(10 * kOhm(tol=0.05))).connected(io=self.ic.creset_b) + + self.mem = imp.Block(SpiMemory(Range.from_lower(self.ic.BITSTREAM_BITS))) + + self.prog = imp.Block(Ice40TargetHeader()) + self.connect(self.prog.reset, self.ic.creset_b) + + # this defaults to flash programming, but to use CRAM programming you can swap the + # SDI/SDO pins on the debug probe and disconnect the CS line + self.spi_merge = self.Block(MergedSpiController()).connected_from(self.ic.spi_config, self.prog.spi) + self.connect(self.spi_merge.out, self.mem.spi) + self.connect(self.ic.spi_config_cs, self.prog.cs) + (self.cs_jmp,), _ = self.chain(self.ic.spi_config_cs, self.Block(DigitalJumper()), self.mem.cs) + # SPI_SS_B is sampled on boot to determine boot config, needs to be high for PROM config + self.mem_pu = imp.Block(PullupResistor(10 * kOhm(tol=0.05))).connected(io=self.mem.cs) + + self.vio_cap0 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.vio_cap1 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.vio_cap2 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.vpp_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + + with self.implicit_connect( # Vcc (core) power domain + ImplicitConnect(self.vcc_reg.pwr_out, [Power]), ImplicitConnect(self.gnd, [Common]) + ) as imp: + self.connect(self.vcc_reg.pwr_out, self.ic.vcc) + self.pll_res = imp.Block(SeriesPowerResistor(100 * Ohm(tol=0.05))) + + self.vcc_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + + with self.implicit_connect( # PLL power domain, section 2 of the iCE40 Hardware Checklist + ImplicitConnect(self.pll_res.pwr_out, [Power]), ImplicitConnect(self.gnd, [Common]) + ) as imp: + self.connect(self.pll_res.pwr_out, self.ic.vcc_pll) + + self.pll_lf = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) + self.pll_hf = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) class Ice40up5k_Sg48(Ice40up): - DEVICE = Ice40up5k_Sg48_Device + DEVICE = Ice40up5k_Sg48_Device diff --git a/edg/parts/Fusb302b.py b/edg/parts/Fusb302b.py index 6a0feea35..8983c3890 100644 --- a/edg/parts/Fusb302b.py +++ b/edg/parts/Fusb302b.py @@ -5,76 +5,79 @@ class Fusb302b_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.vbus = self.Port(VoltageSink(voltage_limits=(4, 21))) - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, - current_draw=(0.37, 40)*uAmp)) # Table 11, between disabled typ and standby typ - self.vconn = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, current_draw=(0, 560)*mAmp), optional=True) - self.gnd = self.Port(Ground()) + def __init__(self) -> None: + super().__init__() + self.vbus = self.Port(VoltageSink(voltage_limits=(4, 21))) + self.vdd = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(0.37, 40) * uAmp) + ) # Table 11, between disabled typ and standby typ + self.vconn = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(0, 560) * mAmp), optional=True + ) + self.gnd = self.Port(Ground()) - self.cc = self.Port(UsbCcPort()) # TODO pass in port models? - i2c_model = DigitalBidir( # interestingly, IO maximum voltages are not specified - voltage_out=(0, 0.35)*Volt, # low-level output voltage - current_limits=(-20, 0)*mAmp, # low-level output current limits - input_thresholds=(0.51, 1.32)*Volt, - output_thresholds=(0.35, float('inf')) * Volt, - ) - self.i2c = self.Port(I2cTarget(i2c_model, [0x22])) - self.int_n = self.Port(DigitalSource.low_from_supply(self.gnd), optional=True) + self.cc = self.Port(UsbCcPort()) # TODO pass in port models? + i2c_model = DigitalBidir( # interestingly, IO maximum voltages are not specified + voltage_out=(0, 0.35) * Volt, # low-level output voltage + current_limits=(-20, 0) * mAmp, # low-level output current limits + input_thresholds=(0.51, 1.32) * Volt, + output_thresholds=(0.35, float("inf")) * Volt, + ) + self.i2c = self.Port(I2cTarget(i2c_model, [0x22])) + self.int_n = self.Port(DigitalSource.low_from_supply(self.gnd), optional=True) - @override - def contents(self) -> None: - self.footprint( - 'U', 'Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm', - { - '1': self.cc.cc2, - '2': self.vbus, - '3': self.vdd, - '4': self.vdd, - '5': self.int_n, - '6': self.i2c.scl, - '7': self.i2c.sda, - '8': self.gnd, - '9': self.gnd, - '10': self.cc.cc1, - '11': self.cc.cc1, - '12': self.vconn, - '13': self.vconn, - '14': self.cc.cc2, - '15': self.gnd - }, - mfr='ON Semiconductor', part='FUSB302B11MPX', # actual several compatible variants - datasheet='https://www.onsemi.com/pdf/datasheet/fusb302b-d.pdf' - ) - self.assign(self.lcsc_part, 'C132291') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + self.footprint( + "U", + "Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm", + { + "1": self.cc.cc2, + "2": self.vbus, + "3": self.vdd, + "4": self.vdd, + "5": self.int_n, + "6": self.i2c.scl, + "7": self.i2c.sda, + "8": self.gnd, + "9": self.gnd, + "10": self.cc.cc1, + "11": self.cc.cc1, + "12": self.vconn, + "13": self.vconn, + "14": self.cc.cc2, + "15": self.gnd, + }, + mfr="ON Semiconductor", + part="FUSB302B11MPX", # actual several compatible variants + datasheet="https://www.onsemi.com/pdf/datasheet/fusb302b-d.pdf", + ) + self.assign(self.lcsc_part, "C132291") + self.assign(self.actual_basic_part, False) class Fusb302b(Interface, Block): - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Fusb302b_Device()) - self.pwr = self.Export(self.ic.vdd, [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.vbus = self.Export(self.ic.vbus) - # TODO conditionally generate Vconn capacitor to support powering cables + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Fusb302b_Device()) + self.pwr = self.Export(self.ic.vdd, [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.vbus = self.Export(self.ic.vbus) + # TODO conditionally generate Vconn capacitor to support powering cables - self.cc = self.Export(self.ic.cc) - self.i2c = self.Export(self.ic.i2c) - self.int = self.Export(self.ic.int_n) + self.cc = self.Export(self.ic.cc) + self.i2c = self.Export(self.ic.i2c) + self.int = self.Export(self.ic.int_n) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - # From Figure 18, reference schematic diagram - # minus the I2C pullups and interrupt pullups, which should be checked to be elsewhere - # and the bulk capacitor, which we hope will be elsewhere - self.vdd_cap = ElementDict[DecouplingCapacitor]() - self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.vdd_cap[1] = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + # From Figure 18, reference schematic diagram + # minus the I2C pullups and interrupt pullups, which should be checked to be elsewhere + # and the bulk capacitor, which we hope will be elsewhere + self.vdd_cap = ElementDict[DecouplingCapacitor]() + self.vdd_cap[0] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap[1] = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - # Crecv is specified in the reference schematic, but doesn't show up on open-source example designs + # Crecv is specified in the reference schematic, but doesn't show up on open-source example designs diff --git a/edg/parts/Fuseholder_Nano2.py b/edg/parts/Fuseholder_Nano2.py index 159cd6367..2e8d7762f 100644 --- a/edg/parts/Fuseholder_Nano2.py +++ b/edg/parts/Fuseholder_Nano2.py @@ -8,21 +8,24 @@ class Nano2Fuseholder(Fuse, JlcPart, FootprintBlock): """Littelfuse Nano2 / 154 series fuseholder. Generic versions exist as 1808 fuses. TODO: generate fuse part numbers from a table, currently this only generates the holder""" + @override def contents(self) -> None: super().contents() self.footprint( - 'F', 'Fuse:Fuseholder_Littelfuse_Nano2_154x', + "F", + "Fuse:Fuseholder_Littelfuse_Nano2_154x", { - '1': self.a, - '2': self.b, + "1": self.a, + "2": self.b, }, - mfr='Littelfuse', part='01550900M', - datasheet='https://www.littelfuse.com/assetdocs/littelfuse-fuse-154-series-data-sheet?assetguid=a8a8a462-7295-481b-a91b-d770dabf005b' + mfr="Littelfuse", + part="01550900M", + datasheet="https://www.littelfuse.com/assetdocs/littelfuse-fuse-154-series-data-sheet?assetguid=a8a8a462-7295-481b-a91b-d770dabf005b", ) self.assign(self.lcsc_part, "C108518") self.assign(self.actual_basic_part, False) self.assign(self.actual_trip_current, self.trip_current) # assumed you can find the right fuse self.assign(self.actual_hold_current, self.hold_current) - self.assign(self.actual_voltage_rating, (-125, 125)*Volt) + self.assign(self.actual_voltage_rating, (-125, 125) * Volt) diff --git a/edg/parts/GateDriver_Ir2301.py b/edg/parts/GateDriver_Ir2301.py index 8d3d990cf..2fbbfc118 100644 --- a/edg/parts/GateDriver_Ir2301.py +++ b/edg/parts/GateDriver_Ir2301.py @@ -5,84 +5,88 @@ class Ir2301_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.com = self.Port(Ground(), [Common]) - self.vcc = self.Port(VoltageSink.from_gnd( - self.com, - voltage_limits=(5, 20)*Volt, # recommended operating conditions - current_draw=RangeExpr() - )) + def __init__(self) -> None: + super().__init__() + self.com = self.Port(Ground(), [Common]) + self.vcc = self.Port( + VoltageSink.from_gnd( + self.com, voltage_limits=(5, 20) * Volt, current_draw=RangeExpr() # recommended operating conditions + ) + ) - input_model = DigitalSink.from_supply( - self.com, self.vcc, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # absolute maximum ratings - input_threshold_abs=(0.8, 2.9)*Volt # static electrical characteristics - ) - self.lin = self.Port(input_model) - self.hin = self.Port(input_model) + input_model = DigitalSink.from_supply( + self.com, + self.vcc, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, # absolute maximum ratings + input_threshold_abs=(0.8, 2.9) * Volt, # static electrical characteristics + ) + self.lin = self.Port(input_model) + self.hin = self.Port(input_model) - self.lo = self.Port(DigitalSource.from_supply( - self.com, self.vcc, - current_limits=(-250, 120)*mAmp) # static electrical characteristics: output short circuit pulsed current - ) - self.assign(self.vcc.current_draw, (50, 190)*uAmp + self.lo.link().current_drawn) + self.lo = self.Port( + DigitalSource.from_supply( + self.com, self.vcc, current_limits=(-250, 120) * mAmp + ) # static electrical characteristics: output short circuit pulsed current + ) + self.assign(self.vcc.current_draw, (50, 190) * uAmp + self.lo.link().current_drawn) - self.vs = self.Port(Ground.from_gnd( - self.com, - voltage_limits=(-5, 600) # no current draw since this is a "ground" pin - )) - self.vb = self.Port(VoltageSink.from_gnd( - self.vs, - voltage_limits=(5, 20)*Volt, - current_draw=RangeExpr() - )) - self.ho = self.Port(DigitalSource.from_supply( - self.vs, self.vb, - current_limits=(-250, 120)*mAmp # static electrical characteristics: output short circuit pulsed current - )) - self.assign(self.vb.current_draw, (50, 190)*uAmp + self.ho.link().current_drawn) + self.vs = self.Port( + Ground.from_gnd(self.com, voltage_limits=(-5, 600)) # no current draw since this is a "ground" pin + ) + self.vb = self.Port(VoltageSink.from_gnd(self.vs, voltage_limits=(5, 20) * Volt, current_draw=RangeExpr())) + self.ho = self.Port( + DigitalSource.from_supply( + self.vs, + self.vb, + current_limits=(-250, 120) + * mAmp, # static electrical characteristics: output short circuit pulsed current + ) + ) + self.assign(self.vb.current_draw, (50, 190) * uAmp + self.ho.link().current_drawn) - @override - def contents(self) -> None: - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.vcc, - '2': self.hin, - '3': self.lin, - '4': self.com, - '5': self.lo, - '6': self.vs, - '7': self.ho, - '8': self.vb, - }, - mfr='Infineon Technologies', part='IR2301', - datasheet='https://www.infineon.com/dgdl/ir2301.pdf?fileId=5546d462533600a4015355c97bb216dc' - ) - self.assign(self.lcsc_part, 'C413500') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.vcc, + "2": self.hin, + "3": self.lin, + "4": self.com, + "5": self.lo, + "6": self.vs, + "7": self.ho, + "8": self.vb, + }, + mfr="Infineon Technologies", + part="IR2301", + datasheet="https://www.infineon.com/dgdl/ir2301.pdf?fileId=5546d462533600a4015355c97bb216dc", + ) + self.assign(self.lcsc_part, "C413500") + self.assign(self.actual_basic_part, False) class Ir2301(HalfBridgeDriver, HalfBridgeDriverIndependent): - """IR2301 half-bridge driver supporting 600V offset, 5-20v input, external boot diode, - no shoot through protect, no deadtime.""" - @override - def contents(self) -> None: - super().contents() + """IR2301 half-bridge driver supporting 600V offset, 5-20v input, external boot diode, + no shoot through protect, no deadtime.""" - self.require(~self.has_boot_diode, 'TODO: boot diode generator') + @override + def contents(self) -> None: + super().contents() - self.ic = self.Block(Ir2301_Device()) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.com) - self.connect(self.low_in, self.ic.lin) - self.connect(self.high_in, self.ic.hin) - self.connect(self.low_out, self.ic.lo) - self.connect(self.high_pwr, self.ic.vb) - self.connect(self.high_gnd, self.ic.vs) - self.connect(self.high_out, self.ic.ho) + self.require(~self.has_boot_diode, "TODO: boot diode generator") - self.cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) - # serves as both boot cap and decoupling cap - self.high_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.high_gnd, self.high_pwr) + self.ic = self.Block(Ir2301_Device()) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.com) + self.connect(self.low_in, self.ic.lin) + self.connect(self.high_in, self.ic.hin) + self.connect(self.low_out, self.ic.lo) + self.connect(self.high_pwr, self.ic.vb) + self.connect(self.high_gnd, self.ic.vs) + self.connect(self.high_out, self.ic.ho) + + self.cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + # serves as both boot cap and decoupling cap + self.high_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.high_gnd, self.high_pwr) diff --git a/edg/parts/GateDriver_Ncp3420.py b/edg/parts/GateDriver_Ncp3420.py index 432f7f9d9..884af9171 100644 --- a/edg/parts/GateDriver_Ncp3420.py +++ b/edg/parts/GateDriver_Ncp3420.py @@ -7,104 +7,122 @@ class Ncp3420_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.pgnd = self.Port(Ground(), [Common]) - self.vcc = self.Port(VoltageSink.from_gnd( - self.pgnd, - voltage_limits=(4.6, 13.2)*Volt, # recommended operating conditions - current_draw=RangeExpr() - )) - - input_model = DigitalSink.from_supply( - self.pgnd, self.vcc, - voltage_limit_abs=(-0.3, 6.5)*Volt, - input_threshold_abs=(0.8, 2.0)*Volt - ) - self.in_ = self.Port(input_model) - self.nod = self.Port(input_model) - - self.drvl = self.Port(DigitalSource.from_supply( - self.pgnd, self.vcc, - current_limits=(-self.vcc.link().voltage.lower()/2.5, self.vcc.link().voltage.lower()/3.0) - )) - - self.swn = self.Port(Ground.from_gnd( - self.pgnd, - voltage_limits=(-5, 35) # no current draw since this is a "ground" pin - )) - self.bst = self.Port(VoltageSink.from_gnd( - self.swn, - voltage_limits=(4.6, 15)*Volt, - )) - self.drvh = self.Port(DigitalSource.from_supply( - self.swn, self.bst, - current_limits=(-self.vcc.link().voltage.lower()/2.5, self.vcc.link().voltage.lower()/3.0) - )) - - self.assign(self.vcc.current_draw, (0.7, 5.0)*mAmp + self.drvl.link().current_drawn + - self.drvh.link().current_drawn) # only system supply given - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.bst, - '2': self.in_, - '3': self.nod, - '4': self.vcc, - '5': self.drvl, - '6': self.pgnd, - '7': self.swn, - '8': self.drvh, - }, - mfr='ON Semiconductor', part='NCP3420', - datasheet='https://www.onsemi.com/pdf/datasheet/ncp3420-d.pdf' - ) - self.assign(self.lcsc_part, 'C154600') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + self.pgnd = self.Port(Ground(), [Common]) + self.vcc = self.Port( + VoltageSink.from_gnd( + self.pgnd, + voltage_limits=(4.6, 13.2) * Volt, # recommended operating conditions + current_draw=RangeExpr(), + ) + ) + + input_model = DigitalSink.from_supply( + self.pgnd, self.vcc, voltage_limit_abs=(-0.3, 6.5) * Volt, input_threshold_abs=(0.8, 2.0) * Volt + ) + self.in_ = self.Port(input_model) + self.nod = self.Port(input_model) + + self.drvl = self.Port( + DigitalSource.from_supply( + self.pgnd, + self.vcc, + current_limits=(-self.vcc.link().voltage.lower() / 2.5, self.vcc.link().voltage.lower() / 3.0), + ) + ) + + self.swn = self.Port( + Ground.from_gnd(self.pgnd, voltage_limits=(-5, 35)) # no current draw since this is a "ground" pin + ) + self.bst = self.Port( + VoltageSink.from_gnd( + self.swn, + voltage_limits=(4.6, 15) * Volt, + ) + ) + self.drvh = self.Port( + DigitalSource.from_supply( + self.swn, + self.bst, + current_limits=(-self.vcc.link().voltage.lower() / 2.5, self.vcc.link().voltage.lower() / 3.0), + ) + ) + + self.assign( + self.vcc.current_draw, (0.7, 5.0) * mAmp + self.drvl.link().current_drawn + self.drvh.link().current_drawn + ) # only system supply given + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.bst, + "2": self.in_, + "3": self.nod, + "4": self.vcc, + "5": self.drvl, + "6": self.pgnd, + "7": self.swn, + "8": self.drvh, + }, + mfr="ON Semiconductor", + part="NCP3420", + datasheet="https://www.onsemi.com/pdf/datasheet/ncp3420-d.pdf", + ) + self.assign(self.lcsc_part, "C154600") + self.assign(self.actual_basic_part, False) class Ncp3420(HalfBridgeDriver, HalfBridgeDriverPwm, Resettable, GeneratorBlock): - """Half-bridge driver supporting 35V offset, 4.6-13.2v input, external boot diode, auto-deadtime.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.has_boot_diode, self.high_pwr.is_connected()) - - @override - def contents(self) -> None: - super().contents() - - self.ic = self.Block(Ncp3420_Device()) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.pgnd) - self.connect(self.pwm_in, self.ic.in_) - self.connect(self.reset, self.ic.nod) - self.connect(self.low_out, self.ic.drvl) - self.connect(self.high_gnd, self.ic.swn) - self.connect(self.high_out, self.ic.drvh) - - self.cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) - # serves as both boot cap and decoupling cap - self.high_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.high_gnd, self.ic.bst) - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.high_pwr.is_connected()): - self.connect(self.high_pwr, self.ic.bst) - else: - if self.get(self.has_boot_diode): - self.boot = self.Block(Diode( - self.high_gnd.link().voltage + self.pwr.link().voltage, - self.ic.vcc.current_draw - )) - self.connect(self.boot.cathode.adapt_to(VoltageSource( - voltage_out=self.high_gnd.link().voltage + self.pwr.link().voltage - )), self.ic.bst) - self.connect(self.boot.anode.adapt_to(VoltageSink( - # no independent limits or current draw, reflected in parameters on other pins - )), self.pwr) + """Half-bridge driver supporting 35V offset, 4.6-13.2v input, external boot diode, auto-deadtime.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.has_boot_diode, self.high_pwr.is_connected()) + + @override + def contents(self) -> None: + super().contents() + + self.ic = self.Block(Ncp3420_Device()) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.pgnd) + self.connect(self.pwm_in, self.ic.in_) + self.connect(self.reset, self.ic.nod) + self.connect(self.low_out, self.ic.drvl) + self.connect(self.high_gnd, self.ic.swn) + self.connect(self.high_out, self.ic.drvh) + + self.cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + # serves as both boot cap and decoupling cap + self.high_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.high_gnd, self.ic.bst) + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.high_pwr.is_connected()): + self.connect(self.high_pwr, self.ic.bst) + else: + if self.get(self.has_boot_diode): + self.boot = self.Block( + Diode(self.high_gnd.link().voltage + self.pwr.link().voltage, self.ic.vcc.current_draw) + ) + self.connect( + self.boot.cathode.adapt_to( + VoltageSource(voltage_out=self.high_gnd.link().voltage + self.pwr.link().voltage) + ), + self.ic.bst, + ) + self.connect( + self.boot.anode.adapt_to( + VoltageSink( + # no independent limits or current draw, reflected in parameters on other pins + ) + ), + self.pwr, + ) diff --git a/edg/parts/GateDriver_Ucc27282.py b/edg/parts/GateDriver_Ucc27282.py index ba4c01a6d..e25314ab8 100644 --- a/edg/parts/GateDriver_Ucc27282.py +++ b/edg/parts/GateDriver_Ucc27282.py @@ -5,84 +5,90 @@ class Ucc27282_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vss = self.Port(Ground(), [Common]) - self.vdd = self.Port(VoltageSink.from_gnd( - self.vss, - voltage_limits=(5.5, 16)*Volt, # recommended operating conditions - current_draw=RangeExpr() - )) + def __init__(self) -> None: + super().__init__() + self.vss = self.Port(Ground(), [Common]) + self.vdd = self.Port( + VoltageSink.from_gnd( + self.vss, voltage_limits=(5.5, 16) * Volt, current_draw=RangeExpr() # recommended operating conditions + ) + ) - input_model = DigitalSink.from_supply( - self.vss, self.vdd, - voltage_limit_abs=(-5, 20)*Volt, - input_threshold_abs=(0.9, 2.4)*Volt - ) - self.li = self.Port(input_model) - self.hi = self.Port(input_model) + input_model = DigitalSink.from_supply( + self.vss, self.vdd, voltage_limit_abs=(-5, 20) * Volt, input_threshold_abs=(0.9, 2.4) * Volt + ) + self.li = self.Port(input_model) + self.hi = self.Port(input_model) - self.lo = self.Port(DigitalSource.from_supply( - self.vss, self.vdd, - current_limits=(-3, 3)*Amp # peak pullup and pulldown current - )) + self.lo = self.Port( + DigitalSource.from_supply( + self.vss, self.vdd, current_limits=(-3, 3) * Amp # peak pullup and pulldown current + ) + ) - self.hs = self.Port(Ground.from_gnd( - self.vss, - voltage_limits=(-8, 100) # looser negative rating possible with pulses - )) - self.hb = self.Port(VoltageSource( - voltage_out=self.hs.link().voltage + self.vdd.link().voltage, - )) - self.ho = self.Port(DigitalSource.from_supply( - self.hs, self.hb, - current_limits=(-3, 3)*Amp # peak pullup and pulldown current - )) + self.hs = self.Port( + Ground.from_gnd(self.vss, voltage_limits=(-8, 100)) # looser negative rating possible with pulses + ) + self.hb = self.Port( + VoltageSource( + voltage_out=self.hs.link().voltage + self.vdd.link().voltage, + ) + ) + self.ho = self.Port( + DigitalSource.from_supply( + self.hs, self.hb, current_limits=(-3, 3) * Amp # peak pullup and pulldown current + ) + ) - # quiescent to operating, vdd and hb, plus output draw - self.assign(self.vdd.current_draw, - (0.3, 4.5)*mAmp + (0.2, 4)*mAmp + self.lo.link().current_drawn + self.ho.link().current_drawn) + # quiescent to operating, vdd and hb, plus output draw + self.assign( + self.vdd.current_draw, + (0.3, 4.5) * mAmp + (0.2, 4) * mAmp + self.lo.link().current_drawn + self.ho.link().current_drawn, + ) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.vdd, - '2': self.hb, - '3': self.ho, - '4': self.hs, - '5': self.hi, - '6': self.li, - '7': self.vss, - '8': self.lo, - }, - mfr='Texas Instruments', part='UCC27282DR', - datasheet='https://www.ti.com/lit/ds/symlink/ucc27282-q1.pdf' - ) - self.assign(self.lcsc_part, 'C2867844') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.vdd, + "2": self.hb, + "3": self.ho, + "4": self.hs, + "5": self.hi, + "6": self.li, + "7": self.vss, + "8": self.lo, + }, + mfr="Texas Instruments", + part="UCC27282DR", + datasheet="https://www.ti.com/lit/ds/symlink/ucc27282-q1.pdf", + ) + self.assign(self.lcsc_part, "C2867844") + self.assign(self.actual_basic_part, False) class Ucc27282(HalfBridgeDriver, HalfBridgeDriverIndependent): - """UCC27282 half-bridge driver supporting 100V offset, 5.5-16v input, internal boot diode, - shoot through protect, no deadtime.""" - @override - def contents(self) -> None: - super().contents() + """UCC27282 half-bridge driver supporting 100V offset, 5.5-16v input, internal boot diode, + shoot through protect, no deadtime.""" - self.require(self.has_boot_diode) - self.require(~self.high_pwr.is_connected()) # not supported + @override + def contents(self) -> None: + super().contents() - self.ic = self.Block(Ucc27282_Device()) - self.connect(self.pwr, self.ic.vdd) - self.connect(self.gnd, self.ic.vss) - self.connect(self.low_in, self.ic.li) - self.connect(self.high_in, self.ic.hi) - self.connect(self.low_out, self.ic.lo) - self.connect(self.high_gnd, self.ic.hs) - self.connect(self.high_out, self.ic.ho) + self.require(self.has_boot_diode) + self.require(~self.high_pwr.is_connected()) # not supported - self.cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.boot_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.ic.hs, self.ic.hb) + self.ic = self.Block(Ucc27282_Device()) + self.connect(self.pwr, self.ic.vdd) + self.connect(self.gnd, self.ic.vss) + self.connect(self.low_in, self.ic.li) + self.connect(self.high_in, self.ic.hi) + self.connect(self.low_out, self.ic.lo) + self.connect(self.high_gnd, self.ic.hs) + self.connect(self.high_out, self.ic.ho) + + self.cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.boot_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.ic.hs, self.ic.hb) diff --git a/edg/parts/Imu_Lsm6ds3trc.py b/edg/parts/Imu_Lsm6ds3trc.py index 6966f9a6f..19a27ed41 100644 --- a/edg/parts/Imu_Lsm6ds3trc.py +++ b/edg/parts/Imu_Lsm6ds3trc.py @@ -7,21 +7,22 @@ class Lsm6ds3trc_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(1.71, 3.6)*Volt, - current_draw=(3*uAmp, 0.9*mAmp) # typical values for low-power and high-performance modes - )) - self.vddio = self.Port(VoltageSink( - voltage_limits=(1.62*Volt, self.vdd.voltage_limits.upper()+0.1) - )) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(1.71, 3.6) * Volt, + current_draw=(3 * uAmp, 0.9 * mAmp), # typical values for low-power and high-performance modes + ) + ) + self.vddio = self.Port(VoltageSink(voltage_limits=(1.62 * Volt, self.vdd.voltage_limits.upper() + 0.1))) self.gnd = self.Port(Ground()) dio_model = DigitalBidir.from_supply( - self.gnd, self.vddio, - voltage_limit_abs=(-0.3*Volt, self.vddio.voltage_limits.upper()+0.3), + self.gnd, + self.vddio, + voltage_limit_abs=(-0.3 * Volt, self.vddio.voltage_limits.upper() + 0.3), # datasheet states abs volt to be [0.3, VDD_IO+0.3], likely a typo - current_limits=(-4, 4)*mAmp, - input_threshold_factor=(0.3, 0.7) + current_limits=(-4, 4) * mAmp, + input_threshold_factor=(0.3, 0.7), ) self.i2c = self.Port(I2cTarget(dio_model)) @@ -32,33 +33,36 @@ def __init__(self) -> None: @override def contents(self) -> None: self.footprint( - 'U', 'Package_LGA:Bosch_LGA-14_3x2.5mm_P0.5mm', + "U", + "Package_LGA:Bosch_LGA-14_3x2.5mm_P0.5mm", { - '1': self.gnd, # sa0 - '2': self.gnd, # not used in mode 1 - '3': self.gnd, # not used in mode 1 - '4': self.int1, - '5': self.vddio, - '6': self.gnd, - '7': self.gnd, - '8': self.vdd, - '9': self.int2, + "1": self.gnd, # sa0 + "2": self.gnd, # not used in mode 1 + "3": self.gnd, # not used in mode 1 + "4": self.int1, + "5": self.vddio, + "6": self.gnd, + "7": self.gnd, + "8": self.vdd, + "9": self.int2, # '10': self.nc, # leave unconnected # '11': self.nc, # leave unconnected - '12': self.vddio, # cs pin - '13': self.i2c.scl, - '14': self.i2c.sda, + "12": self.vddio, # cs pin + "13": self.i2c.scl, + "14": self.i2c.sda, }, - mfr='STMicroelectronics', part='LSM6DS3TR-C', - datasheet='https://www.st.com/resource/en/datasheet/lsm6ds3tr-c.pdf' + mfr="STMicroelectronics", + part="LSM6DS3TR-C", + datasheet="https://www.st.com/resource/en/datasheet/lsm6ds3tr-c.pdf", ) - self.assign(self.lcsc_part, 'C967633') + self.assign(self.lcsc_part, "C967633") self.assign(self.actual_basic_part, False) class Lsm6ds3trc(Accelerometer, Gyroscope, DefaultExportBlock): """Integrated 3d accelerometer (ranging over +/- 2/4/8/16 g) and 3d gyroscope (ranging over +/- 125/250/500/1000/2000 dps).""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Lsm6ds3trc_Device()) @@ -73,5 +77,5 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.vdd_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) - self.vddio_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vddio) + self.vdd_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + self.vddio_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vddio) diff --git a/edg/parts/Ina219.py b/edg/parts/Ina219.py index 8ed7739c2..839caa35a 100644 --- a/edg/parts/Ina219.py +++ b/edg/parts/Ina219.py @@ -10,23 +10,20 @@ def __init__(self, addr_lsb: IntLike): self.addr_lsb = self.ArgParameter(addr_lsb) self.generator_param(self.addr_lsb) - self.vs = self.Port(VoltageSink( - voltage_limits=(3, 5.5) * Volt, - current_draw=(6 * uAmp, 1 * mAmp) - )) + self.vs = self.Port(VoltageSink(voltage_limits=(3, 5.5) * Volt, current_draw=(6 * uAmp, 1 * mAmp))) self.gnd = self.Port(Ground()) self.i2c = self.Port(I2cTarget(DigitalBidir.empty(), addresses=[0x40 + self.addr_lsb])) - self.i2c.sda.init_from(DigitalBidir.from_supply( - self.gnd, self.vs, - voltage_limit_abs=(-0.3, 6.0) * Volt, - input_threshold_factor=(0.3, 0.7) - )) - self.i2c.scl.init_from(DigitalSink.from_supply( - self.gnd, self.vs, - voltage_limit_tolerance=(-0.3, 0.3) * Volt, - input_threshold_factor=(0.3, 0.7) - )) + self.i2c.sda.init_from( + DigitalBidir.from_supply( + self.gnd, self.vs, voltage_limit_abs=(-0.3, 6.0) * Volt, input_threshold_factor=(0.3, 0.7) + ) + ) + self.i2c.scl.init_from( + DigitalSink.from_supply( + self.gnd, self.vs, voltage_limit_tolerance=(-0.3, 0.3) * Volt, input_threshold_factor=(0.3, 0.7) + ) + ) self.in_pos = self.Port(AnalogSink(voltage_limits=(-0.3, 26) * Volt)) self.in_neg = self.Port(AnalogSink(voltage_limits=(-0.3, 26) * Volt)) @@ -55,19 +52,21 @@ def generate(self) -> None: }[self.get(self.addr_lsb)] self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-8', + "U", + "Package_TO_SOT_SMD:SOT-23-8", { - '1': self.in_pos, - '2': self.in_neg, - '3': self.gnd, - '4': self.vs, - '5': self.i2c.scl, - '6': self.i2c.sda, - '7': sa0_pin, - '8': sa1_pin, + "1": self.in_pos, + "2": self.in_neg, + "3": self.gnd, + "4": self.vs, + "5": self.i2c.scl, + "6": self.i2c.sda, + "7": sa0_pin, + "8": sa1_pin, }, - mfr='Texas Instruments', part='INA219AIDCNR', - datasheet='https://www.ti.com/lit/ds/symlink/ina219.pdf' + mfr="Texas Instruments", + part="INA219AIDCNR", + datasheet="https://www.ti.com/lit/ds/symlink/ina219.pdf", ) self.assign(self.lcsc_part, "C87469") self.assign(self.actual_basic_part, False) @@ -85,9 +84,11 @@ def __init__(self, shunt_resistor: RangeLike = 2.0 * mOhm(tol=0.05), *, addr_lsb self.vs_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.Rs = self.Block(CurrentSenseResistor( - resistance=shunt_resistor, - )) + self.Rs = self.Block( + CurrentSenseResistor( + resistance=shunt_resistor, + ) + ) self.sense_pos = self.Export(self.Rs.pwr_in) self.sense_neg = self.Export(self.Rs.pwr_out) diff --git a/edg/parts/Inamp_Ina826.py b/edg/parts/Inamp_Ina826.py index 7e25a26cc..57a40d4dd 100644 --- a/edg/parts/Inamp_Ina826.py +++ b/edg/parts/Inamp_Ina826.py @@ -7,115 +7,134 @@ class Ina826_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vsp = self.Port(VoltageSink( - voltage_limits=(3, 36)*Volt, current_draw=(200, 300)*uAmp # over temperature range, typ to max - ), [Power]) - self.vsn = self.Port(Ground(), [Common]) - - analog_in_model = AnalogSink.from_supply( - self.vsn, self.vsp, - voltage_limit_tolerance=(-2, 40)*Volt, # Vs- - 40 to Vs+ + 40, but down to Vs- - 2 without clamping input res - signal_limit_bound=(0*Volt, -1*Volt), - impedance=2000*MOhm(tol=0) # 20 GOhm typ differential - ) - self.inp = self.Port(analog_in_model) - self.inn = self.Port(analog_in_model) - self.ref = self.Port(AnalogSink.from_supply( - self.vsn, self.vsp, - signal_limit_tolerance=(0, 0)*Volt, # V- to V+ - impedance=100*kOhm(tol=0) # typ - )) - self.out = self.Port(AnalogSource.from_supply( - self.vsn, self.vsp, - signal_out_bound=(0.1*Volt, -0.15*Volt), - current_limits=(-16, 16)*mAmp, # continuous to Vs/2 - impedance=100*Ohm(tol=0) # no tolerance bounds given on datasheet; open-loop impedance - )) - - self.rg2 = self.Port(Passive()) - self.rg3 = self.Port(Passive()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.inn, - '2': self.rg2, - '3': self.rg3, - '4': self.inp, - '5': self.vsn, - '6': self.ref, - '7': self.out, - '8': self.vsp, - }, - mfr='Texas Instruments', part='INA826AIDR', - datasheet='https://www.ti.com/lit/ds/symlink/ina826.pdf' - ) - self.assign(self.lcsc_part, 'C38433') - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + self.vsp = self.Port( + VoltageSink( + voltage_limits=(3, 36) * Volt, current_draw=(200, 300) * uAmp # over temperature range, typ to max + ), + [Power], + ) + self.vsn = self.Port(Ground(), [Common]) + + analog_in_model = AnalogSink.from_supply( + self.vsn, + self.vsp, + voltage_limit_tolerance=(-2, 40) + * Volt, # Vs- - 40 to Vs+ + 40, but down to Vs- - 2 without clamping input res + signal_limit_bound=(0 * Volt, -1 * Volt), + impedance=2000 * MOhm(tol=0), # 20 GOhm typ differential + ) + self.inp = self.Port(analog_in_model) + self.inn = self.Port(analog_in_model) + self.ref = self.Port( + AnalogSink.from_supply( + self.vsn, self.vsp, signal_limit_tolerance=(0, 0) * Volt, impedance=100 * kOhm(tol=0) # V- to V+ # typ + ) + ) + self.out = self.Port( + AnalogSource.from_supply( + self.vsn, + self.vsp, + signal_out_bound=(0.1 * Volt, -0.15 * Volt), + current_limits=(-16, 16) * mAmp, # continuous to Vs/2 + impedance=100 * Ohm(tol=0), # no tolerance bounds given on datasheet; open-loop impedance + ) + ) + + self.rg2 = self.Port(Passive()) + self.rg3 = self.Port(Passive()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.inn, + "2": self.rg2, + "3": self.rg3, + "4": self.inp, + "5": self.vsn, + "6": self.ref, + "7": self.out, + "8": self.vsp, + }, + mfr="Texas Instruments", + part="INA826AIDR", + datasheet="https://www.ti.com/lit/ds/symlink/ina826.pdf", + ) + self.assign(self.lcsc_part, "C38433") + self.assign(self.actual_basic_part, False) class Ina826(KiCadImportableBlock, GeneratorBlock): - """Cost-effective instrumentation amplifier in SOIC-8, with gain 1-1000 set by single resistor. - TODO: DiffAmp / InAmp abstract class, which supports KiCadImportableBlock - """ - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - mapping: Dict[str, Dict[str, BasePort]] = { - 'Simulation_SPICE:OPAMP': { # reference pin not supported - '+': self.input_positive, '-': self.input_negative, '3': self.output, - 'V+': self.pwr, 'V-': self.gnd - }, - 'edg_importable:DifferentialAmplifier': { - '+': self.input_positive, '-': self.input_negative, - 'R': self.output_reference, '3': self.output, - 'V+': self.pwr, 'V-': self.gnd - } - } - return mapping[symbol_name] - - def __init__(self, ratio: RangeLike = 10*Ratio(tol=0.05)): - super().__init__() - self.ic = self.Block(Ina826_Device()) - self.gnd = self.Export(self.ic.vsn) - self.pwr = self.Export(self.ic.vsp) - - self.input_negative = self.Export(self.ic.inn) - self.input_positive = self.Export(self.ic.inp) - self.output_reference = self.Export(self.ic.ref) - self.output = self.Port(AnalogSource.empty()) - - self.ratio = self.ArgParameter(ratio) - self.actual_ratio = self.Parameter(RangeExpr()) - self.generator_param(self.ratio) - - @override - def generate(self) -> None: - super().generate() - - # Datasheet section 8.1: decoupling caps placed as close to device pins as possible - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) - - # gain error, lumped into the resistor gain - self.require(self.ratio.within(Range(1, 1000))) - # note, worst case gain at +/- 0.04% - self.rg = self.Block(Resistor((1/(self.ratio - 1)).shrink_multiply(49.4*kOhm(tol=0.0004)))) - self.connect(self.rg.a, self.ic.rg2) - self.connect(self.rg.b, self.ic.rg3) - - self.assign(self.actual_ratio, 1 + 49.4*kOhm(tol=0.0004) / self.rg.actual_resistance) - output_neg_signal = self.output_reference.is_connected().then_else( - self.output_reference.link().signal, self.gnd.link().voltage - ) - input_diff_range = self.input_positive.link().signal - self.input_negative.link().signal - output_diff_range = input_diff_range * self.actual_ratio + output_neg_signal - self.forced = self.Block(ForcedAnalogSignal(self.ic.out.signal_out.intersect(output_diff_range))) - - self.connect(self.forced.signal_in, self.ic.out) - self.connect(self.forced.signal_out, self.output) + """Cost-effective instrumentation amplifier in SOIC-8, with gain 1-1000 set by single resistor. + TODO: DiffAmp / InAmp abstract class, which supports KiCadImportableBlock + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + mapping: Dict[str, Dict[str, BasePort]] = { + "Simulation_SPICE:OPAMP": { # reference pin not supported + "+": self.input_positive, + "-": self.input_negative, + "3": self.output, + "V+": self.pwr, + "V-": self.gnd, + }, + "edg_importable:DifferentialAmplifier": { + "+": self.input_positive, + "-": self.input_negative, + "R": self.output_reference, + "3": self.output, + "V+": self.pwr, + "V-": self.gnd, + }, + } + return mapping[symbol_name] + + def __init__(self, ratio: RangeLike = 10 * Ratio(tol=0.05)): + super().__init__() + self.ic = self.Block(Ina826_Device()) + self.gnd = self.Export(self.ic.vsn) + self.pwr = self.Export(self.ic.vsp) + + self.input_negative = self.Export(self.ic.inn) + self.input_positive = self.Export(self.ic.inp) + self.output_reference = self.Export(self.ic.ref) + self.output = self.Port(AnalogSource.empty()) + + self.ratio = self.ArgParameter(ratio) + self.actual_ratio = self.Parameter(RangeExpr()) + self.generator_param(self.ratio) + + @override + def generate(self) -> None: + super().generate() + + # Datasheet section 8.1: decoupling caps placed as close to device pins as possible + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) + + # gain error, lumped into the resistor gain + self.require(self.ratio.within(Range(1, 1000))) + # note, worst case gain at +/- 0.04% + self.rg = self.Block(Resistor((1 / (self.ratio - 1)).shrink_multiply(49.4 * kOhm(tol=0.0004)))) + self.connect(self.rg.a, self.ic.rg2) + self.connect(self.rg.b, self.ic.rg3) + + self.assign(self.actual_ratio, 1 + 49.4 * kOhm(tol=0.0004) / self.rg.actual_resistance) + output_neg_signal = self.output_reference.is_connected().then_else( + self.output_reference.link().signal, self.gnd.link().voltage + ) + input_diff_range = self.input_positive.link().signal - self.input_negative.link().signal + output_diff_range = input_diff_range * self.actual_ratio + output_neg_signal + self.forced = self.Block(ForcedAnalogSignal(self.ic.out.signal_out.intersect(output_diff_range))) + + self.connect(self.forced.signal_in, self.ic.out) + self.connect(self.forced.signal_out, self.output) diff --git a/edg/parts/IoExpander_Pca9554.py b/edg/parts/IoExpander_Pca9554.py index 101daaf0a..33ec44e4f 100644 --- a/edg/parts/IoExpander_Pca9554.py +++ b/edg/parts/IoExpander_Pca9554.py @@ -8,91 +8,100 @@ class Pca9554_Device(PinMappable, InternalSubcircuit, FootprintBlock, JlcPart, GeneratorBlock): - def __init__(self, addr_lsb: IntLike, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.gnd = self.Port(Ground()) - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.3, 5.5)*Volt, - current_draw=(0.25, 700)*uAmp # TODO propagate current draw from loads - )) - - i2c_model = DigitalBidir.from_supply( - self.gnd, self.vdd, - current_limits=(-3, 0)*mAmp, - voltage_limit_tolerance=(-0.5, 0.5)*Volt, - input_threshold_factor=(0.3, 0.7) - ) - self.i2c = self.Port(I2cTarget(i2c_model, addresses=ArrayIntExpr())) - - self.io = self.Port(Vector(DigitalBidir.empty()), optional=True) - - self.addr_lsb = self.ArgParameter(addr_lsb) - self.generator_param(self.addr_lsb, self.pin_assigns, self.io.requested()) - - @override - def generate(self) -> None: - dout_model = DigitalBidir.from_supply( # same between TI and NXP versions - self.gnd, self.vdd, - current_limits=(-10, 10)*mAmp, # sink min @ 2.3v (-24mA typ @ 4.5v), source @ 2.3v - voltage_limit_abs=(-0.5, 5.5)*Volt, - input_threshold_abs=(0.8, 2.0)*Volt, - pullup_capable=True # always pullup - ) - - addr_lsb = self.get(self.addr_lsb) - self.require((addr_lsb < 8) & (addr_lsb >= 0), f"addr_lsb={addr_lsb} must be within [0, 8)") - self.assign(self.i2c.addresses, [0x38 | addr_lsb]) # -A variant; otherwise 0x20 | lsb - - pinmaps = PinMapUtil([ # *PW (TSSOP16) variant - PinResource('4', {'IO0': dout_model}), - PinResource('5', {'IO1': dout_model}), - PinResource('6', {'IO2': dout_model}), - PinResource('7', {'IO3': dout_model}), - PinResource('9', {'IO4': dout_model}), - PinResource('10', {'IO5': dout_model}), - PinResource('11', {'IO6': dout_model}), - PinResource('12', {'IO7': dout_model}), - ]) - - ic_pins: Dict[str, CircuitPort] = { - '1': self.vdd if addr_lsb & 1 else self.gnd, # A0 - '2': self.vdd if addr_lsb & 2 else self.gnd, # A1 - '3': self.vdd if addr_lsb & 4 else self.gnd, # A2 - '8': self.gnd, - # '13': self.int, - '14': self.i2c.scl, - '15': self.i2c.sda, - '16': self.vdd, - } - - allocated = pinmaps.allocate([(DigitalBidir, self.get(self.io.requested()))], self.get(self.pin_assigns)) - io_pins: Dict[str, CircuitPort] = { - allocation.pin: self.io.append_elt(dout_model, allocation.name) # type: ignore - for allocation in allocated} - self.generator_set_allocation(allocated) - - self.footprint( - 'U', 'Package_SO:TSSOP-16_4.4x5mm_P0.65mm', - dict(chain(ic_pins.items(), io_pins.items())), - mfr='NXP', part='PCA9554APW,118', # -A variant, in TSSOP16 - datasheet='https://www.nxp.com/docs/en/data-sheet/PCA9554_9554A.pdf' - ) - self.assign(self.lcsc_part, 'C86803') + def __init__(self, addr_lsb: IntLike, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.gnd = self.Port(Ground()) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(2.3, 5.5) * Volt, + current_draw=(0.25, 700) * uAmp, # TODO propagate current draw from loads + ) + ) + + i2c_model = DigitalBidir.from_supply( + self.gnd, + self.vdd, + current_limits=(-3, 0) * mAmp, + voltage_limit_tolerance=(-0.5, 0.5) * Volt, + input_threshold_factor=(0.3, 0.7), + ) + self.i2c = self.Port(I2cTarget(i2c_model, addresses=ArrayIntExpr())) + + self.io = self.Port(Vector(DigitalBidir.empty()), optional=True) + + self.addr_lsb = self.ArgParameter(addr_lsb) + self.generator_param(self.addr_lsb, self.pin_assigns, self.io.requested()) + + @override + def generate(self) -> None: + dout_model = DigitalBidir.from_supply( # same between TI and NXP versions + self.gnd, + self.vdd, + current_limits=(-10, 10) * mAmp, # sink min @ 2.3v (-24mA typ @ 4.5v), source @ 2.3v + voltage_limit_abs=(-0.5, 5.5) * Volt, + input_threshold_abs=(0.8, 2.0) * Volt, + pullup_capable=True, # always pullup + ) + + addr_lsb = self.get(self.addr_lsb) + self.require((addr_lsb < 8) & (addr_lsb >= 0), f"addr_lsb={addr_lsb} must be within [0, 8)") + self.assign(self.i2c.addresses, [0x38 | addr_lsb]) # -A variant; otherwise 0x20 | lsb + + pinmaps = PinMapUtil( + [ # *PW (TSSOP16) variant + PinResource("4", {"IO0": dout_model}), + PinResource("5", {"IO1": dout_model}), + PinResource("6", {"IO2": dout_model}), + PinResource("7", {"IO3": dout_model}), + PinResource("9", {"IO4": dout_model}), + PinResource("10", {"IO5": dout_model}), + PinResource("11", {"IO6": dout_model}), + PinResource("12", {"IO7": dout_model}), + ] + ) + + ic_pins: Dict[str, CircuitPort] = { + "1": self.vdd if addr_lsb & 1 else self.gnd, # A0 + "2": self.vdd if addr_lsb & 2 else self.gnd, # A1 + "3": self.vdd if addr_lsb & 4 else self.gnd, # A2 + "8": self.gnd, + # '13': self.int, + "14": self.i2c.scl, + "15": self.i2c.sda, + "16": self.vdd, + } + + allocated = pinmaps.allocate([(DigitalBidir, self.get(self.io.requested()))], self.get(self.pin_assigns)) + io_pins: Dict[str, CircuitPort] = { + allocation.pin: self.io.append_elt(dout_model, allocation.name) for allocation in allocated # type: ignore + } + self.generator_set_allocation(allocated) + + self.footprint( + "U", + "Package_SO:TSSOP-16_4.4x5mm_P0.65mm", + dict(chain(ic_pins.items(), io_pins.items())), + mfr="NXP", + part="PCA9554APW,118", # -A variant, in TSSOP16 + datasheet="https://www.nxp.com/docs/en/data-sheet/PCA9554_9554A.pdf", + ) + self.assign(self.lcsc_part, "C86803") class Pca9554(IoExpander, PinMappable): - """8 bit I2C IO expander""" - def __init__(self, addr_lsb: IntLike = 0) -> None: - super().__init__() - self.ic = self.Block(Pca9554_Device(addr_lsb=addr_lsb, pin_assigns=self.pin_assigns)) - self.pwr = self.Export(self.ic.vdd, [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.i2c = self.Export(self.ic.i2c) - self.io = self.Export(self.ic.io) - - @override - def contents(self) -> None: - super().contents() - self.assign(self.actual_pin_assigns, self.ic.actual_pin_assigns) - # interestingly, the datasheet doesn't actually call for a local 0.1uF - self.vdd_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + """8 bit I2C IO expander""" + + def __init__(self, addr_lsb: IntLike = 0) -> None: + super().__init__() + self.ic = self.Block(Pca9554_Device(addr_lsb=addr_lsb, pin_assigns=self.pin_assigns)) + self.pwr = self.Export(self.ic.vdd, [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.i2c = self.Export(self.ic.i2c) + self.io = self.Export(self.ic.io) + + @override + def contents(self) -> None: + super().contents() + self.assign(self.actual_pin_assigns, self.ic.actual_pin_assigns) + # interestingly, the datasheet doesn't actually call for a local 0.1uF + self.vdd_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/IoExpander_Pcf8574.py b/edg/parts/IoExpander_Pcf8574.py index f07ca4ba4..34e958ec5 100644 --- a/edg/parts/IoExpander_Pcf8574.py +++ b/edg/parts/IoExpander_Pcf8574.py @@ -8,90 +8,98 @@ class Pcf8574_Device(PinMappable, InternalSubcircuit, FootprintBlock, JlcPart, GeneratorBlock): - def __init__(self, addr_lsb: IntLike, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.gnd = self.Port(Ground()) - self.vdd = self.Port(VoltageSink( # same between TI and NXP versions - voltage_limits=(2.5, 6)*Volt, - current_draw=(2.5, 100)*uAmp # TODO propagate current draw from loads - )) - - i2c_model = DigitalBidir.from_supply( # same between TI and NXP versions - self.gnd, self.vdd, - current_limits=(-3, 0)*mAmp, - voltage_limit_tolerance=(-0.5, 0.5)*Volt, - input_threshold_factor=(0.3, 0.7) - ) - self.i2c = self.Port(I2cTarget(i2c_model, addresses=ArrayIntExpr())) - - self.io = self.Port(Vector(DigitalBidir.empty()), optional=True) - - self.addr_lsb = self.ArgParameter(addr_lsb) - self.generator_param(self.addr_lsb, self.pin_assigns, self.io.requested()) - - @override - def generate(self) -> None: - super().generate() - dout_model = DigitalBidir.from_supply( # same between TI and NXP versions - self.gnd, self.vdd, - current_limits=(-25, 0.3)*mAmp, # highly limited sourcing current - voltage_limit_tolerance=(-0.5, 0.5)*Volt, - input_threshold_factor=(0.3, 0.7) - ) - - addr_lsb = self.get(self.addr_lsb) - self.require((addr_lsb < 8) & (addr_lsb >= 0), f"addr_lsb={addr_lsb} must be within [0, 8)") - self.assign(self.i2c.addresses, [0x20 | addr_lsb]) - - pinmaps = PinMapUtil([ - PinResource('4', {'P0': dout_model}), - PinResource('5', {'P1': dout_model}), - PinResource('6', {'P2': dout_model}), - PinResource('7', {'P3': dout_model}), - PinResource('9', {'P4': dout_model}), - PinResource('10', {'P5': dout_model}), - PinResource('11', {'P6': dout_model}), - PinResource('12', {'P7': dout_model}), - ]) - - ic_pins: Dict[str, CircuitPort] = { - '1': self.vdd if addr_lsb & 1 else self.gnd, # A0 - '2': self.vdd if addr_lsb & 2 else self.gnd, # A1 - '3': self.vdd if addr_lsb & 4 else self.gnd, # A2 - '8': self.gnd, - # '13': self.int, - '14': self.i2c.scl, - '15': self.i2c.sda, - '16': self.vdd, - } - - allocated = pinmaps.allocate([(DigitalBidir, self.get(self.io.requested()))], self.get(self.pin_assigns)) - io_pins: Dict[str, CircuitPort] = { - allocation.pin: self.io.append_elt(dout_model, allocation.name) # type: ignore - for allocation in allocated} - self.generator_set_allocation(allocated) - - self.footprint( - 'U', 'Package_SO:SOIC-16W_7.5x10.3mm_P1.27mm', - dict(chain(ic_pins.items(), io_pins.items())), - mfr='NXP', part='PCF8574AT', - datasheet='https://www.nxp.com/docs/en/data-sheet/PCF8574_PCF8574A.pdf' - ) - self.assign(self.lcsc_part, "C86832") + def __init__(self, addr_lsb: IntLike, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.gnd = self.Port(Ground()) + self.vdd = self.Port( + VoltageSink( # same between TI and NXP versions + voltage_limits=(2.5, 6) * Volt, current_draw=(2.5, 100) * uAmp # TODO propagate current draw from loads + ) + ) + + i2c_model = DigitalBidir.from_supply( # same between TI and NXP versions + self.gnd, + self.vdd, + current_limits=(-3, 0) * mAmp, + voltage_limit_tolerance=(-0.5, 0.5) * Volt, + input_threshold_factor=(0.3, 0.7), + ) + self.i2c = self.Port(I2cTarget(i2c_model, addresses=ArrayIntExpr())) + + self.io = self.Port(Vector(DigitalBidir.empty()), optional=True) + + self.addr_lsb = self.ArgParameter(addr_lsb) + self.generator_param(self.addr_lsb, self.pin_assigns, self.io.requested()) + + @override + def generate(self) -> None: + super().generate() + dout_model = DigitalBidir.from_supply( # same between TI and NXP versions + self.gnd, + self.vdd, + current_limits=(-25, 0.3) * mAmp, # highly limited sourcing current + voltage_limit_tolerance=(-0.5, 0.5) * Volt, + input_threshold_factor=(0.3, 0.7), + ) + + addr_lsb = self.get(self.addr_lsb) + self.require((addr_lsb < 8) & (addr_lsb >= 0), f"addr_lsb={addr_lsb} must be within [0, 8)") + self.assign(self.i2c.addresses, [0x20 | addr_lsb]) + + pinmaps = PinMapUtil( + [ + PinResource("4", {"P0": dout_model}), + PinResource("5", {"P1": dout_model}), + PinResource("6", {"P2": dout_model}), + PinResource("7", {"P3": dout_model}), + PinResource("9", {"P4": dout_model}), + PinResource("10", {"P5": dout_model}), + PinResource("11", {"P6": dout_model}), + PinResource("12", {"P7": dout_model}), + ] + ) + + ic_pins: Dict[str, CircuitPort] = { + "1": self.vdd if addr_lsb & 1 else self.gnd, # A0 + "2": self.vdd if addr_lsb & 2 else self.gnd, # A1 + "3": self.vdd if addr_lsb & 4 else self.gnd, # A2 + "8": self.gnd, + # '13': self.int, + "14": self.i2c.scl, + "15": self.i2c.sda, + "16": self.vdd, + } + + allocated = pinmaps.allocate([(DigitalBidir, self.get(self.io.requested()))], self.get(self.pin_assigns)) + io_pins: Dict[str, CircuitPort] = { + allocation.pin: self.io.append_elt(dout_model, allocation.name) for allocation in allocated # type: ignore + } + self.generator_set_allocation(allocated) + + self.footprint( + "U", + "Package_SO:SOIC-16W_7.5x10.3mm_P1.27mm", + dict(chain(ic_pins.items(), io_pins.items())), + mfr="NXP", + part="PCF8574AT", + datasheet="https://www.nxp.com/docs/en/data-sheet/PCF8574_PCF8574A.pdf", + ) + self.assign(self.lcsc_part, "C86832") class Pcf8574(IoExpander, PinMappable): - """8 bit I2C IO expander with 'quasi-bidirectional IOs'""" - def __init__(self, addr_lsb: IntLike = 0) -> None: - super().__init__() - self.ic = self.Block(Pcf8574_Device(addr_lsb=addr_lsb, pin_assigns=self.pin_assigns)) - self.pwr = self.Export(self.ic.vdd, [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.i2c = self.Export(self.ic.i2c) - self.io = self.Export(self.ic.io) - - @override - def contents(self) -> None: - super().contents() - self.assign(self.actual_pin_assigns, self.ic.actual_pin_assigns) - self.vdd_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + """8 bit I2C IO expander with 'quasi-bidirectional IOs'""" + + def __init__(self, addr_lsb: IntLike = 0) -> None: + super().__init__() + self.ic = self.Block(Pcf8574_Device(addr_lsb=addr_lsb, pin_assigns=self.pin_assigns)) + self.pwr = self.Export(self.ic.vdd, [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.i2c = self.Export(self.ic.i2c) + self.io = self.Export(self.ic.io) + + @override + def contents(self) -> None: + super().contents() + self.assign(self.actual_pin_assigns, self.ic.actual_pin_assigns) + self.vdd_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/Isolator_Cbmud1200.py b/edg/parts/Isolator_Cbmud1200.py index 468891e58..8b2b39cc7 100644 --- a/edg/parts/Isolator_Cbmud1200.py +++ b/edg/parts/Isolator_Cbmud1200.py @@ -7,82 +7,85 @@ class Cbmud1200l_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.gnd1 = self.Port(Ground()) - self.vdd1 = self.Port(VoltageSink.from_gnd( - self.gnd1, - voltage_limits=(2.5, 5.5)*Volt, - current_draw=(280, 300)*uAmp + 2*((0, 26)*mAmp) # 2.5-5v static to maximum dynamic power - )) - in_model = DigitalSink.from_supply( - self.gnd1, self.vdd1, - voltage_limit_tolerance=(-0.3, 0.5), - input_threshold_factor=(0.3, 0.7) - ) - self.via = self.Port(in_model) - self.vib = self.Port(in_model) + def __init__(self) -> None: + super().__init__() + self.gnd1 = self.Port(Ground()) + self.vdd1 = self.Port( + VoltageSink.from_gnd( + self.gnd1, + voltage_limits=(2.5, 5.5) * Volt, + current_draw=(280, 300) * uAmp + 2 * ((0, 26) * mAmp), # 2.5-5v static to maximum dynamic power + ) + ) + in_model = DigitalSink.from_supply( + self.gnd1, self.vdd1, voltage_limit_tolerance=(-0.3, 0.5), input_threshold_factor=(0.3, 0.7) + ) + self.via = self.Port(in_model) + self.vib = self.Port(in_model) - self.gnd2 = self.Port(Ground()) - self.vdd2 = self.Port(VoltageSink.from_gnd( # assumed the same as vdd1 ratings - self.gnd2, - voltage_limits=(2.5, 5.5)*Volt, - current_draw=(280, 300)*uAmp + 2*((0, 26)*mAmp) # 2.5-5v static to maximum dynamic power - )) - out_model = DigitalSource.from_supply( - self.gnd2, self.vdd2, - current_limits=(-50, 50)*mAmp - ) - self.voa = self.Port(out_model) - self.vob = self.Port(out_model) + self.gnd2 = self.Port(Ground()) + self.vdd2 = self.Port( + VoltageSink.from_gnd( # assumed the same as vdd1 ratings + self.gnd2, + voltage_limits=(2.5, 5.5) * Volt, + current_draw=(280, 300) * uAmp + 2 * ((0, 26) * mAmp), # 2.5-5v static to maximum dynamic power + ) + ) + out_model = DigitalSource.from_supply(self.gnd2, self.vdd2, current_limits=(-50, 50) * mAmp) + self.voa = self.Port(out_model) + self.vob = self.Port(out_model) - @override - def contents(self) -> None: - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.vdd1, - '2': self.via, - '3': self.vib, - '4': self.gnd1, - '5': self.gnd2, - '6': self.vob, - '7': self.voa, - '8': self.vdd2, - }, - mfr='Corebai', part='CBMuD1200L', - datasheet='http://corebai.com/en/UploadFiles/20220908/142534811.pdf' - ) - self.assign(self.lcsc_part, 'C476470') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.vdd1, + "2": self.via, + "3": self.vib, + "4": self.gnd1, + "5": self.gnd2, + "6": self.vob, + "7": self.voa, + "8": self.vdd2, + }, + mfr="Corebai", + part="CBMuD1200L", + datasheet="http://corebai.com/en/UploadFiles/20220908/142534811.pdf", + ) + self.assign(self.lcsc_part, "C476470") + self.assign(self.actual_basic_part, False) class Cbmud1200l(DigitalIsolator, GeneratorBlock): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - @override - def generate(self) -> None: - super().generate() - assert not self.get(self.in_b.requested()) and not self.get(self.out_a.requested()), f"device has no b->a channels" + @override + def generate(self) -> None: + super().generate() + assert not self.get(self.in_b.requested()) and not self.get( + self.out_a.requested() + ), f"device has no b->a channels" - self.ic = self.Block(Cbmud1200l_Device()) - self.connect(self.pwr_a, self.ic.vdd1) - self.connect(self.gnd_a, self.ic.gnd1) - self.connect(self.pwr_b, self.ic.vdd2) - self.connect(self.gnd_b, self.ic.gnd2) + self.ic = self.Block(Cbmud1200l_Device()) + self.connect(self.pwr_a, self.ic.vdd1) + self.connect(self.gnd_a, self.ic.gnd1) + self.connect(self.pwr_b, self.ic.vdd2) + self.connect(self.gnd_b, self.ic.gnd2) - channel_pairs = [ - (self.ic.via, self.ic.voa), - (self.ic.vib, self.ic.vob), - ] + channel_pairs = [ + (self.ic.via, self.ic.voa), + (self.ic.vib, self.ic.vob), + ] - self.cap_a = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd_a, self.pwr_a) - self.cap_b = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd_b, self.pwr_b) + self.cap_a = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd_a, self.pwr_a) + self.cap_b = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd_b, self.pwr_b) - for elt_name, (in_a_port, out_b_port) in zip(self.get(self.in_a.requested()), channel_pairs): - self.connect(self.in_a.append_elt(DigitalSink.empty(), elt_name), in_a_port) - self.connect(self.out_b.append_elt(DigitalSource.empty(), elt_name), out_b_port) + for elt_name, (in_a_port, out_b_port) in zip(self.get(self.in_a.requested()), channel_pairs): + self.connect(self.in_a.append_elt(DigitalSink.empty(), elt_name), in_a_port) + self.connect(self.out_b.append_elt(DigitalSource.empty(), elt_name), out_b_port) - self.out_a.defined() - self.in_b.defined() + self.out_a.defined() + self.in_b.defined() diff --git a/edg/parts/Jacdac.py b/edg/parts/Jacdac.py index 600a1aa8d..471c2ad53 100644 --- a/edg/parts/Jacdac.py +++ b/edg/parts/Jacdac.py @@ -8,6 +8,7 @@ class JacdacDataLink(Link): """Link for the JD_DATA line""" + def __init__(self) -> None: super().__init__() self.nodes = self.Port(Vector(JacdacDataPort.empty())) @@ -16,14 +17,17 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.jd_data = self.connect(self.nodes.map_extract(lambda node: node.jd_data), - self.passives.map_extract(lambda node: node.jd_data), - flatten=True) + self.jd_data = self.connect( + self.nodes.map_extract(lambda node: node.jd_data), + self.passives.map_extract(lambda node: node.jd_data), + flatten=True, + ) self.require(self.nodes.length() > 1, "jd_data connection required") class JacdacDataPort(Bundle[JacdacDataLink]): link_type = JacdacDataLink + def __init__(self, model: Optional[DigitalBidir] = None) -> None: super().__init__() if model is None: # ideal by default @@ -33,6 +37,7 @@ def __init__(self, model: Optional[DigitalBidir] = None) -> None: class JacdacPassivePort(Bundle[JacdacDataLink]): link_type = JacdacDataLink + def __init__(self) -> None: super().__init__() self.jd_data = self.Port(DigitalSink()) # needs to be typed but is as close to passive as possible @@ -41,6 +46,7 @@ def __init__(self) -> None: @abstract_block class JacdacSubcircuit(Interface): """Category for Jacdac subcircuits""" + pass @@ -56,30 +62,35 @@ class JacdacEdgeConnectorBare(JacdacSubcircuit, FootprintBlock, GeneratorBlock): If the power sink (power is sunk into the port and off-board) is connected, is_power_provider indicates whether this port should model the maximum downstream current draw """ + def __init__(self, is_power_provider: BoolLike = False) -> None: super().__init__() self.is_power_provider = self.ArgParameter(is_power_provider) # ports for power source mode self.gnd = self.Port(Ground(), [Common]) - self.jd_pwr_src = self.Port(VoltageSource( - voltage_out=(3.5, 5.5)*Volt, - current_limits=(0, 900)*mAmp - ), optional=True) + self.jd_pwr_src = self.Port( + VoltageSource(voltage_out=(3.5, 5.5) * Volt, current_limits=(0, 900) * mAmp), optional=True + ) - self.jd_pwr_sink = self.Port(VoltageSink( - # if not a power provider, extend the voltage range to directly connect to a power source edge - voltage_limits=self.is_power_provider.then_else((4.3, 5.5)*Volt, (3.5, 5.5)*Amp), - current_draw=self.is_power_provider.then_else((900, 1000)*mAmp, (0, 0)*Amp) - ), optional=True) + self.jd_pwr_sink = self.Port( + VoltageSink( + # if not a power provider, extend the voltage range to directly connect to a power source edge + voltage_limits=self.is_power_provider.then_else((4.3, 5.5) * Volt, (3.5, 5.5) * Amp), + current_draw=self.is_power_provider.then_else((900, 1000) * mAmp, (0, 0) * Amp), + ), + optional=True, + ) # TODO this should be a JacdacDataPort, this is being lazy to avoid defining a bridge and diode adapter - self.jd_data = self.Port(DigitalBidir( - voltage_limits=(0, 3.5)*Volt, - voltage_out=(0, 3.5)*Volt, - input_thresholds=(0.3, 3.0)*Volt, - output_thresholds=(0.3, 3.0)*Volt - )) + self.jd_data = self.Port( + DigitalBidir( + voltage_limits=(0, 3.5) * Volt, + voltage_out=(0, 3.5) * Volt, + input_thresholds=(0.3, 3.0) * Volt, + output_thresholds=(0.3, 3.0) * Volt, + ) + ) self.generator_param(self.jd_pwr_src.is_connected()) @@ -100,35 +111,38 @@ def generate(self) -> None: pwr_node = self.jd_pwr_sink self.footprint( # EC refdes for edge connector - 'EC', 'Jacdac:JD-PEC-02_Prerouted_recessed', + "EC", + "Jacdac:JD-PEC-02_Prerouted_recessed", { - '1': self.jd_data, - '2': self.gnd, - '3': pwr_node, + "1": self.jd_data, + "2": self.gnd, + "3": pwr_node, }, ) class Rclamp0521p(TvsDiode, FootprintBlock, JlcPart): """RCLAMP0521P-N TVS diode in 0402 package, recommended in the Jacdac DDK.""" + @override def contents(self) -> None: super().contents() self.require(self.working_voltage.within(self.actual_working_voltage)) self.require(self.actual_capacitance.within(self.capacitance)) - self.assign(self.actual_working_voltage, (-5, 5)*Volt) - self.assign(self.actual_breakdown_voltage, (-5.8, 5.8)*Volt) - self.assign(self.actual_capacitance, 0.3*pFarad(tol=0)) # only typ given + self.assign(self.actual_working_voltage, (-5, 5) * Volt) + self.assign(self.actual_breakdown_voltage, (-5.8, 5.8) * Volt) + self.assign(self.actual_capacitance, 0.3 * pFarad(tol=0)) # only typ given self.footprint( - 'D', 'Diode_SMD:D_0402_1005Metric', + "D", + "Diode_SMD:D_0402_1005Metric", { - '1': self.cathode, - '2': self.anode, + "1": self.cathode, + "2": self.anode, }, ) - self.assign(self.lcsc_part, 'C2827711') + self.assign(self.lcsc_part, "C2827711") self.assign(self.actual_basic_part, False) @@ -139,6 +153,7 @@ class JacdacEdgeConnector(Connector, JacdacSubcircuit, GeneratorBlock): Requires this KiCad footprint library to be available: https://github.com/mattoppenheim/jacdac """ + def __init__(self, is_power_provider: BoolLike = False) -> None: super().__init__() self.is_power_provider = self.ArgParameter(is_power_provider) @@ -170,18 +185,21 @@ def generate(self) -> None: self.connect(self.jd_data.jd_data, self.conn.jd_data) - (self.status_led, ), _ = self.chain(self.jd_status, imp.Block(IndicatorLed(Led.Orange))) - (self.tvs_jd_pwr, ), _ = self.chain(jd_pwr_node, - imp.Block(ProtectionTvsDiode(working_voltage=(0, 5)*Volt))) + (self.status_led,), _ = self.chain(self.jd_status, imp.Block(IndicatorLed(Led.Orange))) + (self.tvs_jd_pwr,), _ = self.chain( + jd_pwr_node, imp.Block(ProtectionTvsDiode(working_voltage=(0, 5) * Volt)) + ) # "ideally less than 1pF but certainly no more than 4pF" - (self.tvs_jd_data, ), _ = self.chain(self.jd_data.jd_data, - imp.Block(DigitalTvsDiode(working_voltage=(0, 3.3)*Volt, - capacitance=(0, 1)*pFarad))) + (self.tvs_jd_data,), _ = self.chain( + self.jd_data.jd_data, + imp.Block(DigitalTvsDiode(working_voltage=(0, 3.3) * Volt, capacitance=(0, 1) * pFarad)), + ) class JacdacDataInterface(JacdacSubcircuit, Block): """Interface from a Jacdac data bus to a device, including protection and EMI filtering. Does NOT include per-port circuitry like ESD diodes and status LEDs.""" + def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground.empty(), [Common]) @@ -193,20 +211,26 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.ferrite = self.Block(FerriteBead(hf_impedance=(1, float('inf'))*kOhm)) + self.ferrite = self.Block(FerriteBead(hf_impedance=(1, float("inf")) * kOhm)) signal_level = self.signal.link().voltage - self.rc = self.Block(LowPassRc(impedance=220*Ohm(tol=0.05), cutoff_freq=22*MHertz(tol=0.12), - voltage=signal_level)) - clamp_diode_model = Diode(reverse_voltage=(0, signal_level.upper()), current=(0, 0)*Amp) + self.rc = self.Block( + LowPassRc(impedance=220 * Ohm(tol=0.05), cutoff_freq=22 * MHertz(tol=0.12), voltage=signal_level) + ) + clamp_diode_model = Diode(reverse_voltage=(0, signal_level.upper()), current=(0, 0) * Amp) self.clamp_hi = self.Block(clamp_diode_model) self.clamp_lo = self.Block(clamp_diode_model) - self.connect(self.jd_data.jd_data, self.ferrite.a.adapt_to(DigitalBidir( - voltage_out=self.signal.link().voltage, - voltage_limits=self.signal.link().voltage_limits, - input_thresholds=self.signal.link().input_thresholds, - output_thresholds=self.signal.link().output_thresholds - ))) + self.connect( + self.jd_data.jd_data, + self.ferrite.a.adapt_to( + DigitalBidir( + voltage_out=self.signal.link().voltage, + voltage_limits=self.signal.link().voltage_limits, + input_thresholds=self.signal.link().input_thresholds, + output_thresholds=self.signal.link().output_thresholds, + ) + ), + ) self.connect(self.ferrite.b, self.rc.output) self.connect(self.rc.input, self.clamp_hi.anode, self.clamp_lo.cathode) self.connect(self.gnd, self.rc.gnd.adapt_to(Ground()), self.clamp_lo.anode.adapt_to(Ground())) @@ -218,6 +242,7 @@ def contents(self) -> None: class JacdacMountingData1(JacdacSubcircuit, FootprintBlock): """Jacdac mounting hole for data, with a passive-typed port so it doesn't count as a connection for validation purposes.""" + def __init__(self) -> None: super().__init__() self.jd_data = self.Port(JacdacPassivePort()) @@ -226,9 +251,10 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() self.footprint( - 'MH', 'Jacdac:jacdac_hole_DATA_notched_MH1', + "MH", + "Jacdac:jacdac_hole_DATA_notched_MH1", { - 'MH1': self.jd_data.jd_data, + "MH1": self.jd_data.jd_data, }, ) @@ -242,9 +268,10 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() self.footprint( - 'MH', 'Jacdac:jacdac_hole_GND_MH2', + "MH", + "Jacdac:jacdac_hole_GND_MH2", { - 'MH2': self.gnd, + "MH2": self.gnd, }, ) @@ -258,9 +285,10 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() self.footprint( - 'MH', 'Jacdac:jacdac_hole_GND_MH4', + "MH", + "Jacdac:jacdac_hole_GND_MH4", { - 'MH4': self.gnd, + "MH4": self.gnd, }, ) @@ -274,9 +302,10 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() self.footprint( - 'MH', 'Jacdac:jacdac_hole_PWR_MH3', + "MH", + "Jacdac:jacdac_hole_PWR_MH3", { - 'MH3': self.jd_pwr, + "MH3": self.jd_pwr, }, ) diff --git a/edg/parts/JlcAntenna.py b/edg/parts/JlcAntenna.py index d55f0fbcc..ba0f1ac07 100644 --- a/edg/parts/JlcAntenna.py +++ b/edg/parts/JlcAntenna.py @@ -7,62 +7,64 @@ class JlcAntenna(JlcTableSelector, TableAntenna, FootprintBlock): - # abstract Antenna does not define standard footprints, so we cannot mix in PartsTableSelectorFootprint - # to do footprint generation + # abstract Antenna does not define standard footprints, so we cannot mix in PartsTableSelectorFootprint + # to do footprint generation - FOOTPRINT_PIN_MAP = { # no antenna-specific footprints, re-use the diode footprints which have a polarity indicator - 'Diode_SMD:D_0402_1005Metric': '1', - 'Diode_SMD:D_0603_1608Metric': '1', - 'Diode_SMD:D_0805_2012Metric': '1', - 'Diode_SMD:D_1206_3216Metric': '1', - } + FOOTPRINT_PIN_MAP = { # no antenna-specific footprints, re-use the diode footprints which have a polarity indicator + "Diode_SMD:D_0402_1005Metric": "1", + "Diode_SMD:D_0603_1608Metric": "1", + "Diode_SMD:D_0805_2012Metric": "1", + "Diode_SMD:D_1206_3216Metric": "1", + } - # because the description formatting is so inconsistent, the table is just hardcoded here - # instead of trying to parse the parts table - PART_FREQUENCY_IMPEDANCE_POWER_FOOTPRINT_MAP = { - 'C293767': (Range(2320e6, 2580e6), Range.exact(50), Range(0, 1), 'Diode_SMD:D_1206_3216Metric'), - # 'C486319' # requires matching circuit? - # 'C504002' # requires matching circuit? - # 'C504001' # requires matching circuit? - # 'C127629': (Range(2400, 2500)*MHertz, 50*Ohm, None, 'Diode_SMD:D_1206_3216Metric'), # requires matching circuit? - # 'C96742' # part number says 3R400G, which doesn't exist on the datasheet - # 'C239243' # GPS antenna 10mmx3mm - } + # because the description formatting is so inconsistent, the table is just hardcoded here + # instead of trying to parse the parts table + PART_FREQUENCY_IMPEDANCE_POWER_FOOTPRINT_MAP = { + "C293767": (Range(2320e6, 2580e6), Range.exact(50), Range(0, 1), "Diode_SMD:D_1206_3216Metric"), + # 'C486319' # requires matching circuit? + # 'C504002' # requires matching circuit? + # 'C504001' # requires matching circuit? + # 'C127629': (Range(2400, 2500)*MHertz, 50*Ohm, None, 'Diode_SMD:D_1206_3216Metric'), # requires matching circuit? + # 'C96742' # part number says 3R400G, which doesn't exist on the datasheet + # 'C239243' # GPS antenna 10mmx3mm + } - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'Antennas': - return None + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "Antennas": + return None - entry = cls.PART_FREQUENCY_IMPEDANCE_POWER_FOOTPRINT_MAP.get(row[cls.LCSC_PART_HEADER]) - if entry is None: - return None + entry = cls.PART_FREQUENCY_IMPEDANCE_POWER_FOOTPRINT_MAP.get(row[cls.LCSC_PART_HEADER]) + if entry is None: + return None - new_cols: Dict[PartsTableColumn, Any] = {} - new_cols[cls.FREQUENCY_RATING] = entry[0] - new_cols[cls.IMPEDANCE] = entry[1] - new_cols[cls.POWER_RATING] = entry[2] - new_cols[cls.KICAD_FOOTPRINT] = entry[3] - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols + new_cols: Dict[PartsTableColumn, Any] = {} + new_cols[cls.FREQUENCY_RATING] = entry[0] + new_cols[cls.IMPEDANCE] = entry[1] + new_cols[cls.POWER_RATING] = entry[2] + new_cols[cls.KICAD_FOOTPRINT] = entry[3] + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols - return cls._jlc_table().map_new_columns(parse_row) + return cls._jlc_table().map_new_columns(parse_row) - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.gnd.init_from(Ground()) # unused for current parts - self.footprint( - "ANT", row[self.KICAD_FOOTPRINT], - { - self.FOOTPRINT_PIN_MAP[row[self.KICAD_FOOTPRINT]]: self.a, - }, - mfr=row[self.MANUFACTURER_COL], part=row[self.PART_NUMBER_COL], - value=row[self.DESCRIPTION_COL], - datasheet=row[self.DATASHEET_COL] - ) + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.gnd.init_from(Ground()) # unused for current parts + self.footprint( + "ANT", + row[self.KICAD_FOOTPRINT], + { + self.FOOTPRINT_PIN_MAP[row[self.KICAD_FOOTPRINT]]: self.a, + }, + mfr=row[self.MANUFACTURER_COL], + part=row[self.PART_NUMBER_COL], + value=row[self.DESCRIPTION_COL], + datasheet=row[self.DATASHEET_COL], + ) lambda: JlcAntenna() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcBjt.py b/edg/parts/JlcBjt.py index 9e636482c..91fee9f45 100644 --- a/edg/parts/JlcBjt.py +++ b/edg/parts/JlcBjt.py @@ -9,50 +9,50 @@ class JlcBjt(PartsTableSelectorFootprint, JlcTableSelector, TableBjt): - PACKAGE_FOOTPRINT_MAP = { - 'SOT-23': 'Package_TO_SOT_SMD:SOT-23', - 'SOT-23-3': 'Package_TO_SOT_SMD:SOT-23', - - 'SOT-323': 'Package_TO_SOT_SMD:SOT-323_SC-70', - 'SOT-323-3': 'Package_TO_SOT_SMD:SOT-323_SC-70', - 'SC-70': 'Package_TO_SOT_SMD:SOT-323_SC-70', - 'SC-70-3': 'Package_TO_SOT_SMD:SOT-323_SC-70', - - 'SOT-89': 'Package_TO_SOT_SMD:SOT-89-3', - 'SOT-89-3': 'Package_TO_SOT_SMD:SOT-89-3', - } - - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - # the included table is differently formatted than the latest JLC description format - (re.compile("(\S+V) (\S+W) (\S+A) (\S+)@(?:\S+A,\S+V) .*(NPN|PNP).* Bipolar Transistors.*"), - lambda match: { - TableBjt.CHANNEL: match.group(5), - TableBjt.VCE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'V')), - TableBjt.ICE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), 'A')), - TableBjt.GAIN: Range.exact(PartParserUtil.parse_value(match.group(4), '')), - TableBjt.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'W')), - }), - ] - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'Bipolar Transistors - BJT': - return None - footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) - if footprint is None: - return None - - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + PACKAGE_FOOTPRINT_MAP = { + "SOT-23": "Package_TO_SOT_SMD:SOT-23", + "SOT-23-3": "Package_TO_SOT_SMD:SOT-23", + "SOT-323": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SOT-323-3": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SC-70": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SC-70-3": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SOT-89": "Package_TO_SOT_SMD:SOT-89-3", + "SOT-89-3": "Package_TO_SOT_SMD:SOT-89-3", + } + + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + # the included table is differently formatted than the latest JLC description format + ( + re.compile("(\S+V) (\S+W) (\S+A) (\S+)@(?:\S+A,\S+V) .*(NPN|PNP).* Bipolar Transistors.*"), + lambda match: { + TableBjt.CHANNEL: match.group(5), + TableBjt.VCE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "V")), + TableBjt.ICE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), "A")), + TableBjt.GAIN: Range.exact(PartParserUtil.parse_value(match.group(4), "")), + TableBjt.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "W")), + }, + ), + ] + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "Bipolar Transistors - BJT": + return None + footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) + if footprint is None: + return None + + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcBjt() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcBlackbox.py b/edg/parts/JlcBlackbox.py index e6c991f11..1a08a7c25 100644 --- a/edg/parts/JlcBlackbox.py +++ b/edg/parts/JlcBlackbox.py @@ -7,45 +7,65 @@ class KiCadJlcBlackbox(KiCadBlackboxBase, JlcPart, FootprintBlock, GeneratorBlock, InternalBlock): - """Similar to KiCadBlackbox, but also supports the lcsc_part field using the symbol's 'JLCPCB Part #'. - This can't extend KiCadBlackbox because KiCadBlackbox.block_from_symbol is non-compositional - """ - @classmethod - @override - def block_from_symbol(cls, symbol: KiCadSymbol, lib: KiCadLibSymbol) -> \ - Tuple['KiCadJlcBlackbox', Callable[['KiCadJlcBlackbox'], Mapping[str, BasePort]]]: - pin_numbers = [pin.number for pin in lib.pins] - refdes_prefix = symbol.properties.get('Refdes Prefix', symbol.refdes.rstrip('0123456789?')) - block_model = KiCadJlcBlackbox( - pin_numbers, refdes_prefix, symbol.properties['Footprint'], - kicad_part=symbol.lib, kicad_value=symbol.properties.get('Value', ''), - kicad_datasheet=symbol.properties.get('Datasheet', ''), - kicad_jlcpcb_part=symbol.properties['JLCPCB Part #']) # required, no .get(...) - def block_pinning(block: KiCadJlcBlackbox) -> Mapping[str, BasePort]: - return {pin: block.ports.request(pin) for pin in pin_numbers} - return block_model, block_pinning - - def __init__(self, kicad_pins: ArrayStringLike, kicad_refdes_prefix: StringLike, kicad_footprint: StringLike, - kicad_part: StringLike, kicad_value: StringLike, kicad_datasheet: StringLike, - kicad_jlcpcb_part: StringLike): - super().__init__() - self.ports = self.Port(Vector(Passive()), optional=True) - self.kicad_refdes_prefix = self.ArgParameter(kicad_refdes_prefix) - self.kicad_footprint = self.ArgParameter(kicad_footprint) - self.kicad_part = self.ArgParameter(kicad_part) - self.kicad_value = self.ArgParameter(kicad_value) - self.kicad_datasheet = self.ArgParameter(kicad_datasheet) - self.assign(self.lcsc_part, kicad_jlcpcb_part) - self.assign(self.actual_basic_part, False) # assumed - - self.kicad_pins = self.ArgParameter(kicad_pins) - self.generator_param(self.kicad_pins) - - @override - def generate(self) -> None: - super().generate() - mapping = {pin_name: self.ports.append_elt(Passive(), pin_name) - for pin_name in self.get(self.kicad_pins)} - self.ports.defined() - self.footprint(self.kicad_refdes_prefix, self.kicad_footprint, mapping, - part=self.kicad_part, value=self.kicad_value, datasheet=self.kicad_datasheet) + """Similar to KiCadBlackbox, but also supports the lcsc_part field using the symbol's 'JLCPCB Part #'. + This can't extend KiCadBlackbox because KiCadBlackbox.block_from_symbol is non-compositional + """ + + @classmethod + @override + def block_from_symbol( + cls, symbol: KiCadSymbol, lib: KiCadLibSymbol + ) -> Tuple["KiCadJlcBlackbox", Callable[["KiCadJlcBlackbox"], Mapping[str, BasePort]]]: + pin_numbers = [pin.number for pin in lib.pins] + refdes_prefix = symbol.properties.get("Refdes Prefix", symbol.refdes.rstrip("0123456789?")) + block_model = KiCadJlcBlackbox( + pin_numbers, + refdes_prefix, + symbol.properties["Footprint"], + kicad_part=symbol.lib, + kicad_value=symbol.properties.get("Value", ""), + kicad_datasheet=symbol.properties.get("Datasheet", ""), + kicad_jlcpcb_part=symbol.properties["JLCPCB Part #"], + ) # required, no .get(...) + + def block_pinning(block: KiCadJlcBlackbox) -> Mapping[str, BasePort]: + return {pin: block.ports.request(pin) for pin in pin_numbers} + + return block_model, block_pinning + + def __init__( + self, + kicad_pins: ArrayStringLike, + kicad_refdes_prefix: StringLike, + kicad_footprint: StringLike, + kicad_part: StringLike, + kicad_value: StringLike, + kicad_datasheet: StringLike, + kicad_jlcpcb_part: StringLike, + ): + super().__init__() + self.ports = self.Port(Vector(Passive()), optional=True) + self.kicad_refdes_prefix = self.ArgParameter(kicad_refdes_prefix) + self.kicad_footprint = self.ArgParameter(kicad_footprint) + self.kicad_part = self.ArgParameter(kicad_part) + self.kicad_value = self.ArgParameter(kicad_value) + self.kicad_datasheet = self.ArgParameter(kicad_datasheet) + self.assign(self.lcsc_part, kicad_jlcpcb_part) + self.assign(self.actual_basic_part, False) # assumed + + self.kicad_pins = self.ArgParameter(kicad_pins) + self.generator_param(self.kicad_pins) + + @override + def generate(self) -> None: + super().generate() + mapping = {pin_name: self.ports.append_elt(Passive(), pin_name) for pin_name in self.get(self.kicad_pins)} + self.ports.defined() + self.footprint( + self.kicad_refdes_prefix, + self.kicad_footprint, + mapping, + part=self.kicad_part, + value=self.kicad_value, + datasheet=self.kicad_datasheet, + ) diff --git a/edg/parts/JlcCapacitor.py b/edg/parts/JlcCapacitor.py index 2069ecd89..3aedf1d85 100644 --- a/edg/parts/JlcCapacitor.py +++ b/edg/parts/JlcCapacitor.py @@ -8,143 +8,159 @@ class JlcCapacitor(JlcTableSelector, PartsTableSelectorFootprint, TableDeratingCapacitor, CeramicCapacitor): - PACKAGE_FOOTPRINT_MAP = { - '0201': 'Capacitor_SMD:C_0201_0603Metric', - '0402': 'Capacitor_SMD:C_0402_1005Metric', - '0603': 'Capacitor_SMD:C_0603_1608Metric', - '0805': 'Capacitor_SMD:C_0805_2012Metric', - '1206': 'Capacitor_SMD:C_1206_3216Metric', - '1210': 'Capacitor_SMD:C_1210_3225Metric', - '1812': 'Capacitor_SMD:C_1812_4532Metric', - - 'C0402': 'Capacitor_SMD:C_0402_1005Metric', - 'C0603': 'Capacitor_SMD:C_0603_1608Metric', - 'C0805': 'Capacitor_SMD:C_0805_2012Metric', - 'C1206': 'Capacitor_SMD:C_1206_3216Metric', - 'C1210': 'Capacitor_SMD:C_1210_3225Metric', - 'C1812': 'Capacitor_SMD:C_1812_4532Metric', - } - DERATE_VOLTCO_MAP = { # in terms of %capacitance / V over 3.6 - 'Capacitor_SMD:C_0201_0603Metric': float('inf'), # not supported, should not generate below 1uF - 'Capacitor_SMD:C_0402_1005Metric': float('inf'), # not supported, should not generate below 1uF - 'Capacitor_SMD:C_0603_1608Metric': float('inf'), # not supported, should not generate below 1uF - 'Capacitor_SMD:C_0805_2012Metric': 0.08, - 'Capacitor_SMD:C_1206_3216Metric': 0.04, - 'Capacitor_SMD:C_1210_3225Metric': 0.04, - 'Capacitor_SMD:C_1812_4532Metric': 0.04, # arbitrary, copy from 1206 - } - - def __init__(self, *args: Any, capacitance_minimum_size: BoolLike = True, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.capacitance_minimum_size = self.ArgParameter(capacitance_minimum_size) - self.generator_param(self.capacitance_minimum_size) - - @classmethod - @override - def _make_table(cls) -> PartsTable: - CAPACITOR_MATCHES = { - 'nominal_capacitance': re.compile(r"(^|\s)([^±]\S+F)($|\s)"), - 'tolerance': re.compile(r"(^|\s)(±\S+[%F])($|\s)"), - 'voltage': re.compile(r"(^|\s)(\d\S*V)($|\s)"), # make sure not to catch 'Y5V' - 'tempco': re.compile(r"(^|\s)([CXYZ]\d[GPRSTUV])($|\s)"), + PACKAGE_FOOTPRINT_MAP = { + "0201": "Capacitor_SMD:C_0201_0603Metric", + "0402": "Capacitor_SMD:C_0402_1005Metric", + "0603": "Capacitor_SMD:C_0603_1608Metric", + "0805": "Capacitor_SMD:C_0805_2012Metric", + "1206": "Capacitor_SMD:C_1206_3216Metric", + "1210": "Capacitor_SMD:C_1210_3225Metric", + "1812": "Capacitor_SMD:C_1812_4532Metric", + "C0402": "Capacitor_SMD:C_0402_1005Metric", + "C0603": "Capacitor_SMD:C_0603_1608Metric", + "C0805": "Capacitor_SMD:C_0805_2012Metric", + "C1206": "Capacitor_SMD:C_1206_3216Metric", + "C1210": "Capacitor_SMD:C_1210_3225Metric", + "C1812": "Capacitor_SMD:C_1812_4532Metric", + } + DERATE_VOLTCO_MAP = { # in terms of %capacitance / V over 3.6 + "Capacitor_SMD:C_0201_0603Metric": float("inf"), # not supported, should not generate below 1uF + "Capacitor_SMD:C_0402_1005Metric": float("inf"), # not supported, should not generate below 1uF + "Capacitor_SMD:C_0603_1608Metric": float("inf"), # not supported, should not generate below 1uF + "Capacitor_SMD:C_0805_2012Metric": 0.08, + "Capacitor_SMD:C_1206_3216Metric": 0.04, + "Capacitor_SMD:C_1210_3225Metric": 0.04, + "Capacitor_SMD:C_1812_4532Metric": 0.04, # arbitrary, copy from 1206 } - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'Multilayer Ceramic Capacitors MLCC - SMD/SMT': - return None - - new_cols: Dict[PartsTableColumn, Any] = {} - try: - # handle the footprint first since this is the most likely to filter - footprint = cls.PACKAGE_FOOTPRINT_MAP[row[cls._PACKAGE_HEADER]] - extracted_values = cls.parse(row[cls.DESCRIPTION_COL], CAPACITOR_MATCHES) - - tempco = extracted_values['tempco'][1] - if tempco[0] not in ('X', 'C') or tempco[2] not in ('R', 'S', 'G'): - return None - - nominal_capacitance = PartParserUtil.parse_value(extracted_values['nominal_capacitance'][1], 'F') - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols[cls.CAPACITANCE] = PartParserUtil.parse_abs_tolerance(extracted_values['tolerance'][1], - nominal_capacitance, 'F') - new_cols[cls.NOMINAL_CAPACITANCE] = nominal_capacitance - - new_cols[cls.VOLTAGE_RATING] = Range.from_abs_tolerance( # voltage rating for ceramic caps is bidirectional - 0, PartParserUtil.parse_value(extracted_values['voltage'][1], 'V')) - new_cols[cls.VOLTCO] = cls.DERATE_VOLTCO_MAP[footprint] - - new_cols.update(cls._parse_jlcpcb_common(row)) - - return new_cols - except (KeyError, PartParserUtil.ParseError): - return None - - return cls._jlc_table().map_new_columns(parse_row) - - @override - def _table_postprocess(self, table: PartsTable) -> PartsTable: - def filter_minimum_size(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - # enforce minimum packages, note the cutoffs are exclusive - nominal_capacitance = row[self.NOMINAL_CAPACITANCE] - footprint = row[self.KICAD_FOOTPRINT] - if nominal_capacitance > 10e-6 and footprint not in [ - 'Capacitor_SMD:C_1206_3216Metric', - ]: - return None - elif nominal_capacitance > 1e-6 and footprint not in [ - 'Capacitor_SMD:C_0805_2012Metric', - 'Capacitor_SMD:C_1206_3216Metric', - ]: - return None - return {} - table = super()._table_postprocess(table) - if self.get(self.capacitance_minimum_size): - table = table.map_new_columns(filter_minimum_size) - return table - - @classmethod - @override - def _row_sort_by(cls, row: PartsTableRow) -> Any: - return [row[cls.PARALLEL_COUNT], super(JlcCapacitor, cls)._row_sort_by(row)] - - @override - def _row_generate(self, row: PartsTableRow) -> None: - # see comment in TableCapacitor._row_generate for why this needs to be here - if row[self.PARALLEL_COUNT] == 1: - super()._row_generate(row) # creates the footprint - else: - TableCapacitor._row_generate(self, row) # skips creating the footprint in PartsTableSelectorFootprint - self.assign(self.actual_basic_part, True) # dummy value - self._make_parallel_footprints(row) - - @override - def _make_parallel_footprints(self, row: PartsTableRow) -> None: - cap_model = JlcDummyCapacitor(set_lcsc_part=row[self.LCSC_PART_HEADER], - set_basic_part=row[self.BASIC_PART_HEADER] == self.BASIC_PART_VALUE, - footprint=row[self.KICAD_FOOTPRINT], - manufacturer=row[self.MANUFACTURER_COL], part_number=row[self.PART_NUMBER_COL], - value=row[self.DESCRIPTION_COL], - capacitance=row[self.NOMINAL_CAPACITANCE], - voltage=self.voltage) - self.c = ElementDict[JlcDummyCapacitor]() - for i in range(row[self.PARALLEL_COUNT]): - self.c[i] = self.Block(cap_model) - self.connect(self.c[i].pos, self.pos) - self.connect(self.c[i].neg, self.neg) + def __init__(self, *args: Any, capacitance_minimum_size: BoolLike = True, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.capacitance_minimum_size = self.ArgParameter(capacitance_minimum_size) + self.generator_param(self.capacitance_minimum_size) + + @classmethod + @override + def _make_table(cls) -> PartsTable: + CAPACITOR_MATCHES = { + "nominal_capacitance": re.compile(r"(^|\s)([^±]\S+F)($|\s)"), + "tolerance": re.compile(r"(^|\s)(±\S+[%F])($|\s)"), + "voltage": re.compile(r"(^|\s)(\d\S*V)($|\s)"), # make sure not to catch 'Y5V' + "tempco": re.compile(r"(^|\s)([CXYZ]\d[GPRSTUV])($|\s)"), + } + + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "Multilayer Ceramic Capacitors MLCC - SMD/SMT": + return None + + new_cols: Dict[PartsTableColumn, Any] = {} + try: + # handle the footprint first since this is the most likely to filter + footprint = cls.PACKAGE_FOOTPRINT_MAP[row[cls._PACKAGE_HEADER]] + extracted_values = cls.parse(row[cls.DESCRIPTION_COL], CAPACITOR_MATCHES) + + tempco = extracted_values["tempco"][1] + if tempco[0] not in ("X", "C") or tempco[2] not in ("R", "S", "G"): + return None + + nominal_capacitance = PartParserUtil.parse_value(extracted_values["nominal_capacitance"][1], "F") + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols[cls.CAPACITANCE] = PartParserUtil.parse_abs_tolerance( + extracted_values["tolerance"][1], nominal_capacitance, "F" + ) + new_cols[cls.NOMINAL_CAPACITANCE] = nominal_capacitance + + new_cols[cls.VOLTAGE_RATING] = ( + Range.from_abs_tolerance( # voltage rating for ceramic caps is bidirectional + 0, PartParserUtil.parse_value(extracted_values["voltage"][1], "V") + ) + ) + new_cols[cls.VOLTCO] = cls.DERATE_VOLTCO_MAP[footprint] + + new_cols.update(cls._parse_jlcpcb_common(row)) + + return new_cols + except (KeyError, PartParserUtil.ParseError): + return None + + return cls._jlc_table().map_new_columns(parse_row) + + @override + def _table_postprocess(self, table: PartsTable) -> PartsTable: + def filter_minimum_size(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + # enforce minimum packages, note the cutoffs are exclusive + nominal_capacitance = row[self.NOMINAL_CAPACITANCE] + footprint = row[self.KICAD_FOOTPRINT] + if nominal_capacitance > 10e-6 and footprint not in [ + "Capacitor_SMD:C_1206_3216Metric", + ]: + return None + elif nominal_capacitance > 1e-6 and footprint not in [ + "Capacitor_SMD:C_0805_2012Metric", + "Capacitor_SMD:C_1206_3216Metric", + ]: + return None + return {} + + table = super()._table_postprocess(table) + if self.get(self.capacitance_minimum_size): + table = table.map_new_columns(filter_minimum_size) + return table + + @classmethod + @override + def _row_sort_by(cls, row: PartsTableRow) -> Any: + return [row[cls.PARALLEL_COUNT], super(JlcCapacitor, cls)._row_sort_by(row)] + + @override + def _row_generate(self, row: PartsTableRow) -> None: + # see comment in TableCapacitor._row_generate for why this needs to be here + if row[self.PARALLEL_COUNT] == 1: + super()._row_generate(row) # creates the footprint + else: + TableCapacitor._row_generate(self, row) # skips creating the footprint in PartsTableSelectorFootprint + self.assign(self.actual_basic_part, True) # dummy value + self._make_parallel_footprints(row) + + @override + def _make_parallel_footprints(self, row: PartsTableRow) -> None: + cap_model = JlcDummyCapacitor( + set_lcsc_part=row[self.LCSC_PART_HEADER], + set_basic_part=row[self.BASIC_PART_HEADER] == self.BASIC_PART_VALUE, + footprint=row[self.KICAD_FOOTPRINT], + manufacturer=row[self.MANUFACTURER_COL], + part_number=row[self.PART_NUMBER_COL], + value=row[self.DESCRIPTION_COL], + capacitance=row[self.NOMINAL_CAPACITANCE], + voltage=self.voltage, + ) + self.c = ElementDict[JlcDummyCapacitor]() + for i in range(row[self.PARALLEL_COUNT]): + self.c[i] = self.Block(cap_model) + self.connect(self.c[i].pos, self.pos) + self.connect(self.c[i].neg, self.neg) class JlcDummyCapacitor(JlcPart, DummyCapacitorFootprint): - """Dummy capacitor that additionally has JLC part fields - """ - def __init__(self, set_lcsc_part: StringLike = "", set_basic_part: BoolLike = False, - footprint: StringLike = "", manufacturer: StringLike = "", - part_number: StringLike = "", value: StringLike = "", *args: Any, **kwargs: Any) -> None: - super().__init__(footprint=footprint, manufacturer=manufacturer, part_number=part_number, - value=value, *args, **kwargs) - - self.assign(self.lcsc_part, set_lcsc_part) - self.assign(self.actual_basic_part, set_basic_part) + """Dummy capacitor that additionally has JLC part fields""" + + def __init__( + self, + set_lcsc_part: StringLike = "", + set_basic_part: BoolLike = False, + footprint: StringLike = "", + manufacturer: StringLike = "", + part_number: StringLike = "", + value: StringLike = "", + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__( + footprint=footprint, manufacturer=manufacturer, part_number=part_number, value=value, *args, **kwargs + ) + + self.assign(self.lcsc_part, set_lcsc_part) + self.assign(self.actual_basic_part, set_basic_part) lambda: JlcCapacitor() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcCrystal.py b/edg/parts/JlcCrystal.py index ae2c51446..36d79591f 100644 --- a/edg/parts/JlcCrystal.py +++ b/edg/parts/JlcCrystal.py @@ -8,55 +8,56 @@ class JlcCrystal(PartsTableSelectorFootprint, JlcTableSelector, TableCrystal): - SERIES_PACKAGE_FOOTPRINT_MAP = { - ('X3225', 'SMD-3225_4P'): 'Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm', - ('TXM', 'SMD-2520_4P'): 'Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm', - } - - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("(\S+Hz) SMD Crystal Resonator (\S+F) (±\S+) .* Crystals .*"), - lambda match: { - TableCrystal.FREQUENCY: PartParserUtil.parse_abs_tolerance( - match.group(3), PartParserUtil.parse_value(match.group(1), 'Hz'), 'Hz'), - TableCrystal.CAPACITANCE: PartParserUtil.parse_value(match.group(2), 'F'), - }), - ] - - CUSTOM_PARTS = { - 'C284176': (Range.from_tolerance(40e6, 10e-6), 15e-12) - } - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'Crystals': - return None - - footprint = None - for (series, package), map_footprint in cls.SERIES_PACKAGE_FOOTPRINT_MAP.items(): - if row[cls.PART_NUMBER_COL].startswith(series) and row[cls._PACKAGE_HEADER] == package: - assert footprint is None, f"multiple footprint rules match {row[cls.PART_NUMBER_COL]}" - footprint = map_footprint - if footprint is None: - return None - - if row[cls.LCSC_PART_HEADER] in cls.CUSTOM_PARTS: - custom_part = cls.CUSTOM_PARTS[row[cls.LCSC_PART_HEADER]] - new_cols: Optional[Dict[PartsTableColumn, Any]] = { - TableCrystal.FREQUENCY: custom_part[0], - TableCrystal.CAPACITANCE: custom_part[1] - } - else: - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + SERIES_PACKAGE_FOOTPRINT_MAP = { + ("X3225", "SMD-3225_4P"): "Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm", + ("TXM", "SMD-2520_4P"): "Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm", + } + + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("(\S+Hz) SMD Crystal Resonator (\S+F) (±\S+) .* Crystals .*"), + lambda match: { + TableCrystal.FREQUENCY: PartParserUtil.parse_abs_tolerance( + match.group(3), PartParserUtil.parse_value(match.group(1), "Hz"), "Hz" + ), + TableCrystal.CAPACITANCE: PartParserUtil.parse_value(match.group(2), "F"), + }, + ), + ] + + CUSTOM_PARTS = {"C284176": (Range.from_tolerance(40e6, 10e-6), 15e-12)} + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "Crystals": + return None + + footprint = None + for (series, package), map_footprint in cls.SERIES_PACKAGE_FOOTPRINT_MAP.items(): + if row[cls.PART_NUMBER_COL].startswith(series) and row[cls._PACKAGE_HEADER] == package: + assert footprint is None, f"multiple footprint rules match {row[cls.PART_NUMBER_COL]}" + footprint = map_footprint + if footprint is None: + return None + + if row[cls.LCSC_PART_HEADER] in cls.CUSTOM_PARTS: + custom_part = cls.CUSTOM_PARTS[row[cls.LCSC_PART_HEADER]] + new_cols: Optional[Dict[PartsTableColumn, Any]] = { + TableCrystal.FREQUENCY: custom_part[0], + TableCrystal.CAPACITANCE: custom_part[1], + } + else: + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcCrystal() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcDiode.py b/edg/parts/JlcDiode.py index 106b1d035..1350e0ff7 100644 --- a/edg/parts/JlcDiode.py +++ b/edg/parts/JlcDiode.py @@ -9,116 +9,132 @@ class JlcBaseDiode: - PACKAGE_FOOTPRINT_MAP = { - 'LL-34': 'Diode_SMD:D_MiniMELF', - 'SOD-123': 'Diode_SMD:D_SOD-123', - 'SOD-123F': 'Diode_SMD:D_SOD-123', - 'SOD-323': 'Diode_SMD:D_SOD-323', - 'DO-214AC(SMA)': 'Diode_SMD:D_SMA', - 'SMA,DO-214AC': 'Diode_SMD:D_SMA', - 'SMA(DO-214AC)': 'Diode_SMD:D_SMA', - 'SMA': 'Diode_SMD:D_SMA', - 'SMAF': 'Diode_SMD:D_SMA', # footprint compatible even if not the same package - 'SMB,DO-214AA': 'Diode_SMD:D_SMB', - 'SMB': 'Diode_SMD:D_SMB', - 'SMC,DO-214AB': 'Diode_SMD:D_SMC', - 'SMC': 'Diode_SMD:D_SMC', - } + PACKAGE_FOOTPRINT_MAP = { + "LL-34": "Diode_SMD:D_MiniMELF", + "SOD-123": "Diode_SMD:D_SOD-123", + "SOD-123F": "Diode_SMD:D_SOD-123", + "SOD-323": "Diode_SMD:D_SOD-323", + "DO-214AC(SMA)": "Diode_SMD:D_SMA", + "SMA,DO-214AC": "Diode_SMD:D_SMA", + "SMA(DO-214AC)": "Diode_SMD:D_SMA", + "SMA": "Diode_SMD:D_SMA", + "SMAF": "Diode_SMD:D_SMA", # footprint compatible even if not the same package + "SMB,DO-214AA": "Diode_SMD:D_SMB", + "SMB": "Diode_SMD:D_SMB", + "SMC,DO-214AB": "Diode_SMD:D_SMC", + "SMC": "Diode_SMD:D_SMC", + } class JlcDiode(PartsTableSelectorFootprint, JlcTableSelector, JlcBaseDiode, TableDiode): - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("(\S+V) (\S+V)@\S+A (\S+A) .* Schottky Barrier Diodes \(SBD\).*"), - lambda match: { - TableDiode.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'V')), - TableDiode.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), 'A')), - TableDiode.FORWARD_VOLTAGE: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'V')), - TableDiode.REVERSE_RECOVERY: Range.zero_to_upper(500e-9), # arbitrary <500ns - }), - (re.compile("(\S+A) (?:Single )?\S+A@\S+V (\S+?V) (\S+V)@\S+A .* General Purpose.*"), - lambda match: { - TableDiode.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'V')), - TableDiode.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'A')), - TableDiode.FORWARD_VOLTAGE: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), 'V')), - TableDiode.REVERSE_RECOVERY: Range.all(), - }), - (re.compile("(\S+V)@\S+A \S+A@\S+V (\S+s) (?:Single )?(\S+A) \S+ (\S+V) .* Diodes - Fast Recovery Rectifiers.*"), - lambda match: { - TableDiode.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(4), 'V')), - TableDiode.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), 'A')), - TableDiode.FORWARD_VOLTAGE: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'V')), - TableDiode.REVERSE_RECOVERY: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 's')), - }), - (re.compile("(\S+V) .*\S+W (?:Single )?(\S+V)@\S+A (\S+s) (?:\S+A@\S+V )?(\S+A) .* Switching Diode.*"), - lambda match: { - TableDiode.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'V')), - TableDiode.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(4), 'A')), - TableDiode.FORWARD_VOLTAGE: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'V')), - TableDiode.REVERSE_RECOVERY: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), 's')), - }), - ] - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] not in [ - 'Schottky Barrier Diodes (SBD)', - 'Diodes - General Purpose', - 'Diodes - Fast Recovery Rectifiers', - 'Switching Diode', - ]: - return None - footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) - if footprint is None: - return None - - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("(\S+V) (\S+V)@\S+A (\S+A) .* Schottky Barrier Diodes \(SBD\).*"), + lambda match: { + TableDiode.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "V")), + TableDiode.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), "A")), + TableDiode.FORWARD_VOLTAGE: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "V")), + TableDiode.REVERSE_RECOVERY: Range.zero_to_upper(500e-9), # arbitrary <500ns + }, + ), + ( + re.compile("(\S+A) (?:Single )?\S+A@\S+V (\S+?V) (\S+V)@\S+A .* General Purpose.*"), + lambda match: { + TableDiode.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "V")), + TableDiode.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "A")), + TableDiode.FORWARD_VOLTAGE: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), "V")), + TableDiode.REVERSE_RECOVERY: Range.all(), + }, + ), + ( + re.compile( + "(\S+V)@\S+A \S+A@\S+V (\S+s) (?:Single )?(\S+A) \S+ (\S+V) .* Diodes - Fast Recovery Rectifiers.*" + ), + lambda match: { + TableDiode.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(4), "V")), + TableDiode.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), "A")), + TableDiode.FORWARD_VOLTAGE: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "V")), + TableDiode.REVERSE_RECOVERY: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "s")), + }, + ), + ( + re.compile("(\S+V) .*\S+W (?:Single )?(\S+V)@\S+A (\S+s) (?:\S+A@\S+V )?(\S+A) .* Switching Diode.*"), + lambda match: { + TableDiode.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "V")), + TableDiode.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(4), "A")), + TableDiode.FORWARD_VOLTAGE: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "V")), + TableDiode.REVERSE_RECOVERY: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), "s")), + }, + ), + ] + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] not in [ + "Schottky Barrier Diodes (SBD)", + "Diodes - General Purpose", + "Diodes - Fast Recovery Rectifiers", + "Switching Diode", + ]: + return None + footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) + if footprint is None: + return None + + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) class JlcZenerDiode(PartsTableSelectorFootprint, JlcTableSelector, JlcBaseDiode, TableZenerDiode): - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("\S+A@\S+V (±\S+%) \S+Ω (?:Single )?(\S+W) (\S+V).* Zener Diodes.*"), - lambda match: { - TableZenerDiode.ZENER_VOLTAGE: PartParserUtil.parse_abs_tolerance( - match.group(1), PartParserUtil.parse_value(match.group(3), 'V'), 'V'), - TableZenerDiode.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'W')), - }), - (re.compile("\S+A@\S+V \S+Ω (?:Single )?(\S+V)~(\S+V) (\S+W) \S+V .* Zener Diodes.*"), - lambda match: { - TableZenerDiode.ZENER_VOLTAGE: Range(PartParserUtil.parse_value(match.group(1), 'V'), - PartParserUtil.parse_value(match.group(2), 'V')), - TableZenerDiode.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), 'W')), - }), - ] - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'Zener Diodes': - return None - footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) - if footprint is None: - return None - - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("\S+A@\S+V (±\S+%) \S+Ω (?:Single )?(\S+W) (\S+V).* Zener Diodes.*"), + lambda match: { + TableZenerDiode.ZENER_VOLTAGE: PartParserUtil.parse_abs_tolerance( + match.group(1), PartParserUtil.parse_value(match.group(3), "V"), "V" + ), + TableZenerDiode.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "W")), + }, + ), + ( + re.compile("\S+A@\S+V \S+Ω (?:Single )?(\S+V)~(\S+V) (\S+W) \S+V .* Zener Diodes.*"), + lambda match: { + TableZenerDiode.ZENER_VOLTAGE: Range( + PartParserUtil.parse_value(match.group(1), "V"), PartParserUtil.parse_value(match.group(2), "V") + ), + TableZenerDiode.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), "W")), + }, + ), + ] + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "Zener Diodes": + return None + footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) + if footprint is None: + return None + + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcDiode(), JlcZenerDiode # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcElectrolyticCapacitor.py b/edg/parts/JlcElectrolyticCapacitor.py index 9b14b743a..ca7e5aaf7 100644 --- a/edg/parts/JlcElectrolyticCapacitor.py +++ b/edg/parts/JlcElectrolyticCapacitor.py @@ -8,46 +8,48 @@ class JlcAluminumCapacitor(PartsTableSelectorFootprint, JlcTableSelector, TableCapacitor, AluminumCapacitor): - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - # the negative match prevents the dimension substring from partially eaten - (re.compile(".* (\S+F).* (\S+V).* (±\S+%).*[^\d\.]([\d\.]+x[\d\.]+)mm Aluminum Electrolytic Capacitors.*"), - lambda match: { # discard the HF impedance parameter - TableCapacitor.NOMINAL_CAPACITANCE: PartParserUtil.parse_value(match.group(1), 'F'), - TableCapacitor.CAPACITANCE: PartParserUtil.parse_abs_tolerance( - match.group(3), PartParserUtil.parse_value(match.group(1), 'F'), 'F'), - TableCapacitor.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'V')), - - JlcTableSelector.KICAD_FOOTPRINT: f"Capacitor_SMD:CP_Elec_{match.group(4)}", - }), - ] - - def __init__(self, *args: Any, capacitance_minimum_size: BoolLike = True, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.capacitance_minimum_size = self.ArgParameter(capacitance_minimum_size) - self.generator_param(self.capacitance_minimum_size) - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'Aluminum Electrolytic Capacitors - SMD': - return None - - new_cols = {} - - desc_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if desc_cols is not None: - new_cols.update(desc_cols) - else: - return None - - if new_cols[cls.KICAD_FOOTPRINT] not in cls._standard_footprint()._footprint_pinning_map(): - return None - - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + # the negative match prevents the dimension substring from partially eaten + ( + re.compile(".* (\S+F).* (\S+V).* (±\S+%).*[^\d\.]([\d\.]+x[\d\.]+)mm Aluminum Electrolytic Capacitors.*"), + lambda match: { # discard the HF impedance parameter + TableCapacitor.NOMINAL_CAPACITANCE: PartParserUtil.parse_value(match.group(1), "F"), + TableCapacitor.CAPACITANCE: PartParserUtil.parse_abs_tolerance( + match.group(3), PartParserUtil.parse_value(match.group(1), "F"), "F" + ), + TableCapacitor.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "V")), + JlcTableSelector.KICAD_FOOTPRINT: f"Capacitor_SMD:CP_Elec_{match.group(4)}", + }, + ), + ] + + def __init__(self, *args: Any, capacitance_minimum_size: BoolLike = True, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.capacitance_minimum_size = self.ArgParameter(capacitance_minimum_size) + self.generator_param(self.capacitance_minimum_size) + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "Aluminum Electrolytic Capacitors - SMD": + return None + + new_cols = {} + + desc_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if desc_cols is not None: + new_cols.update(desc_cols) + else: + return None + + if new_cols[cls.KICAD_FOOTPRINT] not in cls._standard_footprint()._footprint_pinning_map(): + return None + + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcAluminumCapacitor() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcFerriteBead.py b/edg/parts/JlcFerriteBead.py index 888782d19..5a8202e18 100644 --- a/edg/parts/JlcFerriteBead.py +++ b/edg/parts/JlcFerriteBead.py @@ -9,51 +9,53 @@ class JlcFerriteBead(PartsTableSelectorFootprint, JlcTableSelector, TableFerriteBead): - PACKAGE_FOOTPRINT_MAP = { - '0402': 'Inductor_SMD:L_0402_1005Metric', - '0603': 'Inductor_SMD:L_0603_1608Metric', - '0805': 'Inductor_SMD:L_0805_2012Metric', - '1206': 'Inductor_SMD:L_1206_3216Metric', - '1210': 'Inductor_SMD:L_1210_3225Metric', - '1812': 'Inductor_SMD:L_1812_4532Metric', - - 'L0402': 'Inductor_SMD:L_0402_1005Metric', - 'L0603': 'Inductor_SMD:L_0603_1608Metric', - 'L0805': 'Inductor_SMD:L_0805_2012Metric', - 'L1206': 'Inductor_SMD:L_1206_3216Metric', - 'L1210': 'Inductor_SMD:L_1210_3225Metric', - 'L1812': 'Inductor_SMD:L_1812_4532Metric', - } - - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("(\S+A) (?:\S+ )?(\S+Ω) (\S+Ω)@\S+Hz (±\S+%) .* Ferrite Beads.*"), - lambda match: { # discard the HF impedance parameter - TableFerriteBead.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'A')), - TableFerriteBead.DC_RESISTANCE: Range.exact(PartParserUtil.parse_value(match.group(2), 'Ω')), - TableFerriteBead.HF_IMPEDANCE: PartParserUtil.parse_abs_tolerance( - match.group(4), PartParserUtil.parse_value(match.group(3), 'Ω'), 'Ω'), - }), - ] - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if not row['Second Category'] == 'Ferrite Beads': - return None - footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) - if footprint is None: - return None - - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + PACKAGE_FOOTPRINT_MAP = { + "0402": "Inductor_SMD:L_0402_1005Metric", + "0603": "Inductor_SMD:L_0603_1608Metric", + "0805": "Inductor_SMD:L_0805_2012Metric", + "1206": "Inductor_SMD:L_1206_3216Metric", + "1210": "Inductor_SMD:L_1210_3225Metric", + "1812": "Inductor_SMD:L_1812_4532Metric", + "L0402": "Inductor_SMD:L_0402_1005Metric", + "L0603": "Inductor_SMD:L_0603_1608Metric", + "L0805": "Inductor_SMD:L_0805_2012Metric", + "L1206": "Inductor_SMD:L_1206_3216Metric", + "L1210": "Inductor_SMD:L_1210_3225Metric", + "L1812": "Inductor_SMD:L_1812_4532Metric", + } + + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("(\S+A) (?:\S+ )?(\S+Ω) (\S+Ω)@\S+Hz (±\S+%) .* Ferrite Beads.*"), + lambda match: { # discard the HF impedance parameter + TableFerriteBead.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "A")), + TableFerriteBead.DC_RESISTANCE: Range.exact(PartParserUtil.parse_value(match.group(2), "Ω")), + TableFerriteBead.HF_IMPEDANCE: PartParserUtil.parse_abs_tolerance( + match.group(4), PartParserUtil.parse_value(match.group(3), "Ω"), "Ω" + ), + }, + ), + ] + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if not row["Second Category"] == "Ferrite Beads": + return None + footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) + if footprint is None: + return None + + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcFerriteBead() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcFet.py b/edg/parts/JlcFet.py index d5c802832..6a8fb3dc2 100644 --- a/edg/parts/JlcFet.py +++ b/edg/parts/JlcFet.py @@ -10,195 +10,199 @@ @non_library class FetFallbackGateCharge(PartsTableSelector, BaseTableFet): - """A TableFet that allows a fallback gate charge if not specified in the table. - Unspecified entries must be Range.all(), which will be substituted with the fallback - value in per-Block post-processing.""" - def __init__(self, *args: Any, fallback_gate_charge: RangeLike = Range.from_tolerance(3000e-9, 0), **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - # allow the user to specify a gate charge - self.fallback_gate_charge = self.ArgParameter(fallback_gate_charge) - self.generator_param(self.fallback_gate_charge) - - @override - def _table_postprocess(self, table: PartsTable) -> PartsTable: - fallback_gate_charge = self.get(self.fallback_gate_charge) - def process_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row[TableFet.GATE_CHARGE] == Range.all(): - return {TableFet.GATE_CHARGE: fallback_gate_charge} - else: - return {TableFet.GATE_CHARGE: row[TableFet.GATE_CHARGE]} - - # must run before TableFet power calculations - return super()._table_postprocess(table.map_new_columns(process_row, overwrite=True)) + """A TableFet that allows a fallback gate charge if not specified in the table. + Unspecified entries must be Range.all(), which will be substituted with the fallback + value in per-Block post-processing.""" + + def __init__( + self, *args: Any, fallback_gate_charge: RangeLike = Range.from_tolerance(3000e-9, 0), **kwargs: Any + ) -> None: + super().__init__(*args, **kwargs) + # allow the user to specify a gate charge + self.fallback_gate_charge = self.ArgParameter(fallback_gate_charge) + self.generator_param(self.fallback_gate_charge) + + @override + def _table_postprocess(self, table: PartsTable) -> PartsTable: + fallback_gate_charge = self.get(self.fallback_gate_charge) + + def process_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row[TableFet.GATE_CHARGE] == Range.all(): + return {TableFet.GATE_CHARGE: fallback_gate_charge} + else: + return {TableFet.GATE_CHARGE: row[TableFet.GATE_CHARGE]} + + # must run before TableFet power calculations + return super()._table_postprocess(table.map_new_columns(process_row, overwrite=True)) class JlcBaseFet(JlcTableSelector): - PACKAGE_FOOTPRINT_MAP = { - 'SOT-23': 'Package_TO_SOT_SMD:SOT-23', - 'SOT23-3': 'Package_TO_SOT_SMD:SOT-23', - 'SOT-23-3': 'Package_TO_SOT_SMD:SOT-23', - 'SOT-23-3L': 'Package_TO_SOT_SMD:SOT-23', - - 'SOT-323': 'Package_TO_SOT_SMD:SOT-323_SC-70', - 'SOT-323(SC-80)': 'Package_TO_SOT_SMD:SOT-323_SC-70', - 'SOT-323-3': 'Package_TO_SOT_SMD:SOT-323_SC-70', - 'SC-70-3': 'Package_TO_SOT_SMD:SOT-323_SC-70', - - 'TO-252': 'Package_TO_SOT_SMD:TO-252-2', # aka DPak - 'TO-252-2': 'Package_TO_SOT_SMD:TO-252-2', - 'TO-252(DPAK)': 'Package_TO_SOT_SMD:TO-252-2', - 'DPAK': 'Package_TO_SOT_SMD:TO-252-2', - 'TO-252-2(DPAK)': 'Package_TO_SOT_SMD:TO-252-2', - 'TO-263-2': 'Package_TO_SOT_SMD:TO-263-2', # aka D2Pak - 'D2PAK': 'Package_TO_SOT_SMD:TO-263-2', - - 'SOT-223': 'Package_TO_SOT_SMD:SOT-223-3_TabPin2', - 'SOT-223-3': 'Package_TO_SOT_SMD:SOT-223-3_TabPin2', - - 'SO-8': 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - 'SOIC-8': 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - 'SOIC-8_3.9x4.9x1.27P': 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - 'SOP-8': 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - 'SOP-8_3.9x4.9x1.27P': 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - - 'PowerPAK SO-8_EP_5.2x6.2x1.27P': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'POWERPAK-SO-8': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PowerPAK-SO-8': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PowerPAKSO-8L': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PowerPAKSO-8': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'DFN5X6-8': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'DFN5x6': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'DFN-8_5x6x1.27P': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'DFN-8(5x6)': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'DFN5X6-8L': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PDFN5x6': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PDFN5x6-8': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PDFN5x6-8L': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PDFN5X6-8L': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PQFN 5X6': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PQFN5x6-8': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PQFN-8(4.9x5.8)': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PQFN-8(5x6)': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PRPAK5x6': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PRPAK5x6-8L': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PG-TDSON-8_EP_5.2x6.2x1.27P': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - - 'TDSON-8-EP(5x6)': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PDFN-8(5x6)': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'DFN-8-EP(6.1x5.2)': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'DFN-8(4.9x5.8)': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - 'PDFN-8(5.8x4.9)': 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic', - } - - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("(\S+V) (\S+A) (\S+W) (\S+Ω)@(\S+V),\S+A (\S+V)@\S+A.* ([PN]) Channel.* MOSFETs.*"), - lambda match: { - TableFet.CHANNEL: match.group(7), - TableFet.VDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'V')), - TableFet.IDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'A')), - # Vgs isn't specified, so the Ron@Vgs is used as a lower bound; assumed symmetric - TableFet.VGS_RATING: Range.from_abs_tolerance(0, - PartParserUtil.parse_value(match.group(5), 'V')), - TableFet.VGS_DRIVE: Range(PartParserUtil.parse_value(match.group(6), 'V'), - PartParserUtil.parse_value(match.group(5), 'V')), - TableFet.RDS_ON: Range.exact(PartParserUtil.parse_value(match.group(4), 'Ω')), - TableFet.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), 'W')), - TableFet.GATE_CHARGE: Range.all(), # unspecified - }), - # some are more detailed - (re.compile("(\S+V) (\S+A) (\S+Ω)@(\S+V),\S+A (\S+W) (\S+V)@\S+A.* ([PN]) Channel.* (\S+C)@\S+V.* MOSFETs.*"), - lambda match: { - TableFet.CHANNEL: match.group(7), - TableFet.VDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'V')), - TableFet.IDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'A')), - # Vgs isn't specified, so the Ron@Vgs is used as a lower bound; assumed symmetric - TableFet.VGS_RATING: Range.from_abs_tolerance(0, - PartParserUtil.parse_value(match.group(4), 'V')), - TableFet.VGS_DRIVE: Range(PartParserUtil.parse_value(match.group(6), 'V'), - PartParserUtil.parse_value(match.group(4), 'V')), - TableFet.RDS_ON: Range.exact(PartParserUtil.parse_value(match.group(3), 'Ω')), - TableFet.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(5), 'W')), - TableFet.GATE_CHARGE: Range.exact(PartParserUtil.parse_value(match.group(8), 'C')) - }), - # many still don't have the gate charge - (re.compile("(\S+V) (\S+A) (\S+Ω)@(\S+V),\S+A (\S+W) (\S+V)@\S+A.* ([PN]) Channel.* MOSFETs.*"), - lambda match: { - TableFet.CHANNEL: match.group(7), - TableFet.VDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'V')), - TableFet.IDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), 'A')), - # Vgs isn't specified, so the Ron@Vgs is used as a lower bound; assumed symmetric - TableFet.VGS_RATING: Range.from_abs_tolerance(0, - PartParserUtil.parse_value(match.group(4), 'V')), - TableFet.VGS_DRIVE: Range(PartParserUtil.parse_value(match.group(6), 'V'), - PartParserUtil.parse_value(match.group(4), 'V')), - TableFet.RDS_ON: Range.exact(PartParserUtil.parse_value(match.group(3), 'Ω')), - TableFet.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(5), 'W')), - TableFet.GATE_CHARGE: Range.all(), # unspecified - }), - ] - - SUPPLEMENTAL_QC = { # mfr part number to typ Qc @ max Vgs (if multiple specified) - # QFN-8 devices - 'IRFH7440TRPBF': 92e-9, # @ Vgs=10 - 'BSC028N06NSATMA1': 37e-9, # @ Vgs=0...10V - 'BSC057N08NS3G': 42e-9, # @ Vgs=0...10V - 'BSC093N04LSG': 18e-9, # @ Vgs=0...10V - 'BSC160N10NS3G': 19e-9, # @ Vgs=0...10V - 'SIR876ADP-T1-GE3': 32.8e-9, # @ Vgs=10 - 'SI7336ADP-T1-E3': 36e-9, # @ Vgs=4.5 - 'SIR470DP-T1-GE3': 102e-9, # @ Vgs=10 - - # SOIC-8 devices, top 5 stock in the static parts table - 'AO4406A': 14e-9, # @ Vgs=10 - 'IRF8313TRPBF': 6.0e-9, # @ Vgs=4.5 - 'AO4435': 18e-9, # @ Vgs=-10 - 'AO4419': 19e-9, # @ Vgs=4.5 - 'AO4264E': 14.5e-9, # @ Vgs=10 - 'AO4485': 42e-9, # @ Vgs=10 - 'AO4459': 9.2e-9, # @ Vgs=10 - 'AO4468': 15e-9, # @ Vgs=10 - 'IRF7458TRPBF': 39e-9, # @ Vgs=10 - 'AO4407A': 30e-9, # @ Vgs=10 - - # DPAK devices - 'IRLR024NTRPBF': 15e-9, # @ Vgs=5 - 'AOD413A': 16.2e-9, # @ Vgs=-10 - 'IRLR8726TRPBF': 15e-9, # @ Vgs=4.5 - 'IRLR8726TRLPBF': 15e-9, # @ Vgs=4.5 - 'IRFR5410TRPBF': 58e-9, # @ Vgs=-10 - 'KIA50N03BD': 25e-9, # @ Vgs=10 - } - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'MOSFETs': - return None - footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) - if footprint is None: - return None - - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - if new_cols[TableFet.GATE_CHARGE] == Range.all() and row[cls.PART_NUMBER_COL] in cls.SUPPLEMENTAL_QC: - new_cols[TableFet.GATE_CHARGE] = Range.exact(cls.SUPPLEMENTAL_QC[row[cls.PART_NUMBER_COL]]) - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + PACKAGE_FOOTPRINT_MAP = { + "SOT-23": "Package_TO_SOT_SMD:SOT-23", + "SOT23-3": "Package_TO_SOT_SMD:SOT-23", + "SOT-23-3": "Package_TO_SOT_SMD:SOT-23", + "SOT-23-3L": "Package_TO_SOT_SMD:SOT-23", + "SOT-323": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SOT-323(SC-80)": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SOT-323-3": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SC-70-3": "Package_TO_SOT_SMD:SOT-323_SC-70", + "TO-252": "Package_TO_SOT_SMD:TO-252-2", # aka DPak + "TO-252-2": "Package_TO_SOT_SMD:TO-252-2", + "TO-252(DPAK)": "Package_TO_SOT_SMD:TO-252-2", + "DPAK": "Package_TO_SOT_SMD:TO-252-2", + "TO-252-2(DPAK)": "Package_TO_SOT_SMD:TO-252-2", + "TO-263-2": "Package_TO_SOT_SMD:TO-263-2", # aka D2Pak + "D2PAK": "Package_TO_SOT_SMD:TO-263-2", + "SOT-223": "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + "SOT-223-3": "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + "SO-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "SOIC-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "SOIC-8_3.9x4.9x1.27P": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "SOP-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "SOP-8_3.9x4.9x1.27P": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "PowerPAK SO-8_EP_5.2x6.2x1.27P": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "POWERPAK-SO-8": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PowerPAK-SO-8": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PowerPAKSO-8L": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PowerPAKSO-8": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "DFN5X6-8": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "DFN5x6": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "DFN-8_5x6x1.27P": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "DFN-8(5x6)": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "DFN5X6-8L": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PDFN5x6": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PDFN5x6-8": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PDFN5x6-8L": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PDFN5X6-8L": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PQFN 5X6": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PQFN5x6-8": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PQFN-8(4.9x5.8)": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PQFN-8(5x6)": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PRPAK5x6": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PRPAK5x6-8L": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PG-TDSON-8_EP_5.2x6.2x1.27P": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "TDSON-8-EP(5x6)": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PDFN-8(5x6)": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "DFN-8-EP(6.1x5.2)": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "DFN-8(4.9x5.8)": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + "PDFN-8(5.8x4.9)": "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic", + } + + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("(\S+V) (\S+A) (\S+W) (\S+Ω)@(\S+V),\S+A (\S+V)@\S+A.* ([PN]) Channel.* MOSFETs.*"), + lambda match: { + TableFet.CHANNEL: match.group(7), + TableFet.VDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "V")), + TableFet.IDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "A")), + # Vgs isn't specified, so the Ron@Vgs is used as a lower bound; assumed symmetric + TableFet.VGS_RATING: Range.from_abs_tolerance(0, PartParserUtil.parse_value(match.group(5), "V")), + TableFet.VGS_DRIVE: Range( + PartParserUtil.parse_value(match.group(6), "V"), PartParserUtil.parse_value(match.group(5), "V") + ), + TableFet.RDS_ON: Range.exact(PartParserUtil.parse_value(match.group(4), "Ω")), + TableFet.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(3), "W")), + TableFet.GATE_CHARGE: Range.all(), # unspecified + }, + ), + # some are more detailed + ( + re.compile( + "(\S+V) (\S+A) (\S+Ω)@(\S+V),\S+A (\S+W) (\S+V)@\S+A.* ([PN]) Channel.* (\S+C)@\S+V.* MOSFETs.*" + ), + lambda match: { + TableFet.CHANNEL: match.group(7), + TableFet.VDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "V")), + TableFet.IDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "A")), + # Vgs isn't specified, so the Ron@Vgs is used as a lower bound; assumed symmetric + TableFet.VGS_RATING: Range.from_abs_tolerance(0, PartParserUtil.parse_value(match.group(4), "V")), + TableFet.VGS_DRIVE: Range( + PartParserUtil.parse_value(match.group(6), "V"), PartParserUtil.parse_value(match.group(4), "V") + ), + TableFet.RDS_ON: Range.exact(PartParserUtil.parse_value(match.group(3), "Ω")), + TableFet.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(5), "W")), + TableFet.GATE_CHARGE: Range.exact(PartParserUtil.parse_value(match.group(8), "C")), + }, + ), + # many still don't have the gate charge + ( + re.compile("(\S+V) (\S+A) (\S+Ω)@(\S+V),\S+A (\S+W) (\S+V)@\S+A.* ([PN]) Channel.* MOSFETs.*"), + lambda match: { + TableFet.CHANNEL: match.group(7), + TableFet.VDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "V")), + TableFet.IDS_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(2), "A")), + # Vgs isn't specified, so the Ron@Vgs is used as a lower bound; assumed symmetric + TableFet.VGS_RATING: Range.from_abs_tolerance(0, PartParserUtil.parse_value(match.group(4), "V")), + TableFet.VGS_DRIVE: Range( + PartParserUtil.parse_value(match.group(6), "V"), PartParserUtil.parse_value(match.group(4), "V") + ), + TableFet.RDS_ON: Range.exact(PartParserUtil.parse_value(match.group(3), "Ω")), + TableFet.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(5), "W")), + TableFet.GATE_CHARGE: Range.all(), # unspecified + }, + ), + ] + + SUPPLEMENTAL_QC = { # mfr part number to typ Qc @ max Vgs (if multiple specified) + # QFN-8 devices + "IRFH7440TRPBF": 92e-9, # @ Vgs=10 + "BSC028N06NSATMA1": 37e-9, # @ Vgs=0...10V + "BSC057N08NS3G": 42e-9, # @ Vgs=0...10V + "BSC093N04LSG": 18e-9, # @ Vgs=0...10V + "BSC160N10NS3G": 19e-9, # @ Vgs=0...10V + "SIR876ADP-T1-GE3": 32.8e-9, # @ Vgs=10 + "SI7336ADP-T1-E3": 36e-9, # @ Vgs=4.5 + "SIR470DP-T1-GE3": 102e-9, # @ Vgs=10 + # SOIC-8 devices, top 5 stock in the static parts table + "AO4406A": 14e-9, # @ Vgs=10 + "IRF8313TRPBF": 6.0e-9, # @ Vgs=4.5 + "AO4435": 18e-9, # @ Vgs=-10 + "AO4419": 19e-9, # @ Vgs=4.5 + "AO4264E": 14.5e-9, # @ Vgs=10 + "AO4485": 42e-9, # @ Vgs=10 + "AO4459": 9.2e-9, # @ Vgs=10 + "AO4468": 15e-9, # @ Vgs=10 + "IRF7458TRPBF": 39e-9, # @ Vgs=10 + "AO4407A": 30e-9, # @ Vgs=10 + # DPAK devices + "IRLR024NTRPBF": 15e-9, # @ Vgs=5 + "AOD413A": 16.2e-9, # @ Vgs=-10 + "IRLR8726TRPBF": 15e-9, # @ Vgs=4.5 + "IRLR8726TRLPBF": 15e-9, # @ Vgs=4.5 + "IRFR5410TRPBF": 58e-9, # @ Vgs=-10 + "KIA50N03BD": 25e-9, # @ Vgs=10 + } + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "MOSFETs": + return None + footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) + if footprint is None: + return None + + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + if new_cols[TableFet.GATE_CHARGE] == Range.all() and row[cls.PART_NUMBER_COL] in cls.SUPPLEMENTAL_QC: + new_cols[TableFet.GATE_CHARGE] = Range.exact(cls.SUPPLEMENTAL_QC[row[cls.PART_NUMBER_COL]]) + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) class JlcFet(PartsTableSelectorFootprint, JlcBaseFet, FetFallbackGateCharge, TableFet): - pass + pass class JlcSwitchFet(PartsTableSelectorFootprint, JlcBaseFet, FetFallbackGateCharge, TableSwitchFet): - pass + pass lambda: (JlcFet(), JlcSwitchFet()) # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcInductor.py b/edg/parts/JlcInductor.py index 9dd3ba2f6..30df6e40e 100644 --- a/edg/parts/JlcInductor.py +++ b/edg/parts/JlcInductor.py @@ -9,114 +9,127 @@ class JlcInductor(PartsTableSelectorFootprint, JlcTableSelector, TableInductor): - PACKAGE_FOOTPRINT_MAP = { - '0402': 'Inductor_SMD:L_0402_1005Metric', - '0603': 'Inductor_SMD:L_0603_1608Metric', - '0805': 'Inductor_SMD:L_0805_2012Metric', - '1206': 'Inductor_SMD:L_1206_3216Metric', - '1210': 'Inductor_SMD:L_1210_3225Metric', - '1812': 'Inductor_SMD:L_1812_4532Metric', - - 'L0402': 'Inductor_SMD:L_0402_1005Metric', - 'L0603': 'Inductor_SMD:L_0603_1608Metric', - 'L0805': 'Inductor_SMD:L_0805_2012Metric', - 'L1206': 'Inductor_SMD:L_1206_3216Metric', - 'L1210': 'Inductor_SMD:L_1210_3225Metric', - 'L1812': 'Inductor_SMD:L_1812_4532Metric', - } - - # a secondary parsing method if the package parser fails - PART_FOOTPRINT_PARSERS: List[DescriptionParser] = [ - (re.compile("^NR(20|24|30|40|50|60|80).*$"), - lambda match: { - PartsTableSelectorFootprint.KICAD_FOOTPRINT: f'Inductor_SMD:L_Taiyo-Yuden_NR-{match.group(1)}xx' - }), - (re.compile("^SRP12(45|65)A?-.*$"), - lambda match: { - PartsTableSelectorFootprint.KICAD_FOOTPRINT: f'Inductor_SMD:L_Bourns_SRP1245A' - }), - (re.compile("^SRR1015-.*$"), - lambda match: { - PartsTableSelectorFootprint.KICAD_FOOTPRINT: f'Inductor_SMD:L_Bourns-SRR1005' - }), - (re.compile("^SRR1210A?-.*$"), - lambda match: { - PartsTableSelectorFootprint.KICAD_FOOTPRINT: f'Inductor_SMD:L_Bourns_SRR1210A' - }), - (re.compile("^SRR1260A?-.*$"), - lambda match: { - PartsTableSelectorFootprint.KICAD_FOOTPRINT: f'Inductor_SMD:L_Bourns_SRR1260' - }), - (re.compile("^SWPA(3010|3012|3015|4010|4012|4018|4020|4025|4030|5012|5020|5040|6020|6028|6040|6045|8040)S.*$"), - lambda match: { - PartsTableSelectorFootprint.KICAD_FOOTPRINT: f'Inductor_SMD:L_Sunlord_SWPA{match.group(1)}S' - }), - (re.compile("^SWRB(1204|1205|1207)S.*$"), - lambda match: { - PartsTableSelectorFootprint.KICAD_FOOTPRINT: f'Inductor_SMD:L_Sunlord_SWRB{match.group(1)}S' - }), - (re.compile("^SLF(6025|6028|6045|7032|7045|7055|10145|12555|12565|12575)T.*$"), - lambda match: { - PartsTableSelectorFootprint.KICAD_FOOTPRINT: f'Inductor_SMD:L_TDK_SLF{match.group(1)}' - }), - # Kicad does not have stock 1008 footprint - ] - - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("(\S+A) (\S+H) (±\S+%)(?: \S+A)? (\S+Ω) .* Inductors.*"), - lambda match: { - TableInductor.INDUCTANCE: PartParserUtil.parse_abs_tolerance( - match.group(3), PartParserUtil.parse_value(match.group(2), 'H'), 'H'), - TableInductor.FREQUENCY_RATING: Range.all(), # ignored, checked elsewhere - TableInductor.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'A')), - TableInductor.DC_RESISTANCE: Range.exact(PartParserUtil.parse_value(match.group(4), 'Ω')), - }), - (re.compile("(\S+A) (\S+H) ±(\S+H)(?: \S+A)? (\S+Ω) .* Inductors.*"), - lambda match: { - TableInductor.INDUCTANCE: Range.from_abs_tolerance(PartParserUtil.parse_value(match.group(2), 'H'), - PartParserUtil.parse_value(match.group(3), 'H')), - TableInductor.FREQUENCY_RATING: Range.all(), # ignored, checked elsewhere - TableInductor.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'A')), - TableInductor.DC_RESISTANCE: Range.exact(PartParserUtil.parse_value(match.group(4), 'Ω')), - }), - ] - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - # because the table does not have frequency specs, the table filter can't enforce frequency ratings - # so the user must add the actual frequency rating in refinements - self.manual_frequency_rating = self.Parameter(RangeExpr(Range.exact(0))) - self.require(self.frequency.within(self.manual_frequency_rating)) - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] not in ('Inductors (SMD)', 'Power Inductors'): - return None - - new_cols = {} - - footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) - if footprint is not None: - new_cols[cls.KICAD_FOOTPRINT] = footprint - else: - footprint_cols = cls.parse_full_description(row[cls.PART_NUMBER_COL], cls.PART_FOOTPRINT_PARSERS) - if footprint_cols is not None: - new_cols.update(footprint_cols) - else: - return None - - desc_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if desc_cols is not None: - new_cols.update(desc_cols) - else: - return None - - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + PACKAGE_FOOTPRINT_MAP = { + "0402": "Inductor_SMD:L_0402_1005Metric", + "0603": "Inductor_SMD:L_0603_1608Metric", + "0805": "Inductor_SMD:L_0805_2012Metric", + "1206": "Inductor_SMD:L_1206_3216Metric", + "1210": "Inductor_SMD:L_1210_3225Metric", + "1812": "Inductor_SMD:L_1812_4532Metric", + "L0402": "Inductor_SMD:L_0402_1005Metric", + "L0603": "Inductor_SMD:L_0603_1608Metric", + "L0805": "Inductor_SMD:L_0805_2012Metric", + "L1206": "Inductor_SMD:L_1206_3216Metric", + "L1210": "Inductor_SMD:L_1210_3225Metric", + "L1812": "Inductor_SMD:L_1812_4532Metric", + } + + # a secondary parsing method if the package parser fails + PART_FOOTPRINT_PARSERS: List[DescriptionParser] = [ + ( + re.compile("^NR(20|24|30|40|50|60|80).*$"), + lambda match: { + PartsTableSelectorFootprint.KICAD_FOOTPRINT: f"Inductor_SMD:L_Taiyo-Yuden_NR-{match.group(1)}xx" + }, + ), + ( + re.compile("^SRP12(45|65)A?-.*$"), + lambda match: {PartsTableSelectorFootprint.KICAD_FOOTPRINT: f"Inductor_SMD:L_Bourns_SRP1245A"}, + ), + ( + re.compile("^SRR1015-.*$"), + lambda match: {PartsTableSelectorFootprint.KICAD_FOOTPRINT: f"Inductor_SMD:L_Bourns-SRR1005"}, + ), + ( + re.compile("^SRR1210A?-.*$"), + lambda match: {PartsTableSelectorFootprint.KICAD_FOOTPRINT: f"Inductor_SMD:L_Bourns_SRR1210A"}, + ), + ( + re.compile("^SRR1260A?-.*$"), + lambda match: {PartsTableSelectorFootprint.KICAD_FOOTPRINT: f"Inductor_SMD:L_Bourns_SRR1260"}, + ), + ( + re.compile( + "^SWPA(3010|3012|3015|4010|4012|4018|4020|4025|4030|5012|5020|5040|6020|6028|6040|6045|8040)S.*$" + ), + lambda match: { + PartsTableSelectorFootprint.KICAD_FOOTPRINT: f"Inductor_SMD:L_Sunlord_SWPA{match.group(1)}S" + }, + ), + ( + re.compile("^SWRB(1204|1205|1207)S.*$"), + lambda match: { + PartsTableSelectorFootprint.KICAD_FOOTPRINT: f"Inductor_SMD:L_Sunlord_SWRB{match.group(1)}S" + }, + ), + ( + re.compile("^SLF(6025|6028|6045|7032|7045|7055|10145|12555|12565|12575)T.*$"), + lambda match: {PartsTableSelectorFootprint.KICAD_FOOTPRINT: f"Inductor_SMD:L_TDK_SLF{match.group(1)}"}, + ), + # Kicad does not have stock 1008 footprint + ] + + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("(\S+A) (\S+H) (±\S+%)(?: \S+A)? (\S+Ω) .* Inductors.*"), + lambda match: { + TableInductor.INDUCTANCE: PartParserUtil.parse_abs_tolerance( + match.group(3), PartParserUtil.parse_value(match.group(2), "H"), "H" + ), + TableInductor.FREQUENCY_RATING: Range.all(), # ignored, checked elsewhere + TableInductor.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "A")), + TableInductor.DC_RESISTANCE: Range.exact(PartParserUtil.parse_value(match.group(4), "Ω")), + }, + ), + ( + re.compile("(\S+A) (\S+H) ±(\S+H)(?: \S+A)? (\S+Ω) .* Inductors.*"), + lambda match: { + TableInductor.INDUCTANCE: Range.from_abs_tolerance( + PartParserUtil.parse_value(match.group(2), "H"), PartParserUtil.parse_value(match.group(3), "H") + ), + TableInductor.FREQUENCY_RATING: Range.all(), # ignored, checked elsewhere + TableInductor.CURRENT_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "A")), + TableInductor.DC_RESISTANCE: Range.exact(PartParserUtil.parse_value(match.group(4), "Ω")), + }, + ), + ] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # because the table does not have frequency specs, the table filter can't enforce frequency ratings + # so the user must add the actual frequency rating in refinements + self.manual_frequency_rating = self.Parameter(RangeExpr(Range.exact(0))) + self.require(self.frequency.within(self.manual_frequency_rating)) + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] not in ("Inductors (SMD)", "Power Inductors"): + return None + + new_cols = {} + + footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) + if footprint is not None: + new_cols[cls.KICAD_FOOTPRINT] = footprint + else: + footprint_cols = cls.parse_full_description(row[cls.PART_NUMBER_COL], cls.PART_FOOTPRINT_PARSERS) + if footprint_cols is not None: + new_cols.update(footprint_cols) + else: + return None + + desc_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if desc_cols is not None: + new_cols.update(desc_cols) + else: + return None + + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcInductor() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcLed.py b/edg/parts/JlcLed.py index 8ead12006..d9078ed49 100644 --- a/edg/parts/JlcLed.py +++ b/edg/parts/JlcLed.py @@ -7,57 +7,55 @@ class JlcLed(PartsTableSelectorFootprint, JlcTableSelector, TableLed): - PACKAGE_FOOTPRINT_MAP = { - # 0201 not in parts table, LED_0201_0603Metric - - '0402': 'LED_SMD:LED_0402_1005Metric', - '0603': 'LED_SMD:LED_0603_1608Metric', - '0805': 'LED_SMD:LED_0805_2012Metric', - '1206': 'LED_SMD:LED_1206_3216Metric', - - 'LED_0402': 'LED_SMD:LED_0402_1005Metric', - 'LED_0603': 'LED_SMD:LED_0603_1608Metric', - 'LED_0805': 'LED_SMD:LED_0805_2012Metric', - 'LED_1206': 'LED_SMD:LED_1206_3216Metric', - } - - # because the description formatting is so inconsistent, the table is just hardcoded here - # instead of trying to parse the parts table - PART_COLOR_MAP = { - 'C2286': Led.Red, - 'C2290': Led.White, - 'C2293': Led.Blue, - 'C2296': Led.Yellow, - 'C2297': Led.Green, - 'C34499': Led.White, - 'C72038': Led.Yellow, - 'C72041': Led.Blue, - 'C72043': Led.Green, # "emerald" - 'C84256': Led.Red, - } - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'Light Emitting Diodes (LED)': - return None - - footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) - if footprint is None: - return None - - color = cls.PART_COLOR_MAP.get(row[cls.LCSC_PART_HEADER]) - if color is None: - return None - - new_cols = {} - new_cols[cls.COLOR] = color - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + PACKAGE_FOOTPRINT_MAP = { + # 0201 not in parts table, LED_0201_0603Metric + "0402": "LED_SMD:LED_0402_1005Metric", + "0603": "LED_SMD:LED_0603_1608Metric", + "0805": "LED_SMD:LED_0805_2012Metric", + "1206": "LED_SMD:LED_1206_3216Metric", + "LED_0402": "LED_SMD:LED_0402_1005Metric", + "LED_0603": "LED_SMD:LED_0603_1608Metric", + "LED_0805": "LED_SMD:LED_0805_2012Metric", + "LED_1206": "LED_SMD:LED_1206_3216Metric", + } + + # because the description formatting is so inconsistent, the table is just hardcoded here + # instead of trying to parse the parts table + PART_COLOR_MAP = { + "C2286": Led.Red, + "C2290": Led.White, + "C2293": Led.Blue, + "C2296": Led.Yellow, + "C2297": Led.Green, + "C34499": Led.White, + "C72038": Led.Yellow, + "C72041": Led.Blue, + "C72043": Led.Green, # "emerald" + "C84256": Led.Red, + } + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "Light Emitting Diodes (LED)": + return None + + footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) + if footprint is None: + return None + + color = cls.PART_COLOR_MAP.get(row[cls.LCSC_PART_HEADER]) + if color is None: + return None + + new_cols = {} + new_cols[cls.COLOR] = color + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcLed() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcOscillator.py b/edg/parts/JlcOscillator.py index c1a131d42..a5f1acfcf 100644 --- a/edg/parts/JlcOscillator.py +++ b/edg/parts/JlcOscillator.py @@ -8,111 +8,134 @@ class JlcOscillator_Device(InternalSubcircuit, Block): - """Base oscillator device definition, that takes in the part data from the containing part table. - Defines a standard interface, and specifies the footprint here.""" - FOOTPRINT: str + """Base oscillator device definition, that takes in the part data from the containing part table. + Defines a standard interface, and specifies the footprint here.""" - def __init__(self, in_kicad_part: StringLike, in_kicad_value: StringLike, in_kicad_datasheet: StringLike, - in_lcsc_part: StringLike, in_actual_basic_part: BoolLike): - super().__init__() - self.gnd = self.Port(Ground.empty()) - self.vcc = self.Port(VoltageSink.empty()) - self.out = self.Port(DigitalSource.empty()) + FOOTPRINT: str - self.in_kicad_part = self.ArgParameter(in_kicad_part) - self.in_kicad_value = self.ArgParameter(in_kicad_value) - self.in_kicad_datasheet = self.ArgParameter(in_kicad_datasheet) + def __init__( + self, + in_kicad_part: StringLike, + in_kicad_value: StringLike, + in_kicad_datasheet: StringLike, + in_lcsc_part: StringLike, + in_actual_basic_part: BoolLike, + ): + super().__init__() + self.gnd = self.Port(Ground.empty()) + self.vcc = self.Port(VoltageSink.empty()) + self.out = self.Port(DigitalSource.empty()) - self.in_lcsc_part = self.ArgParameter(in_lcsc_part) - self.in_actual_basic_part = self.ArgParameter(in_actual_basic_part) + self.in_kicad_part = self.ArgParameter(in_kicad_part) + self.in_kicad_value = self.ArgParameter(in_kicad_value) + self.in_kicad_datasheet = self.ArgParameter(in_kicad_datasheet) + + self.in_lcsc_part = self.ArgParameter(in_lcsc_part) + self.in_actual_basic_part = self.ArgParameter(in_actual_basic_part) @non_library class Sg8101_Base_Device(JlcOscillator_Device, JlcPart, FootprintBlock): - FOOTPRINT: str - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.gnd.init_from(Ground()) - self.vcc.init_from(VoltageSink(voltage_limits=(1.62, 3.62)*Volt, - current_draw=(0.0003, 3.5)*mAmp)) # 1.8 stdby typ to 3.3 max - self.out.init_from(DigitalSource.from_supply(self.gnd, self.vcc, - 0*mAmp(tol=0))) - - self.footprint( - 'X', self.FOOTPRINT, - { - '1': self.vcc, # ST/OE pin - '2': self.gnd, - '3': self.out, - '4': self.vcc, - }, - part=self.in_kicad_part, value=self.in_kicad_value, datasheet=self.in_kicad_datasheet) - - self.assign(self.lcsc_part, self.in_lcsc_part) - self.assign(self.actual_basic_part, self.in_actual_basic_part) + FOOTPRINT: str + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.gnd.init_from(Ground()) + self.vcc.init_from( + VoltageSink(voltage_limits=(1.62, 3.62) * Volt, current_draw=(0.0003, 3.5) * mAmp) + ) # 1.8 stdby typ to 3.3 max + self.out.init_from(DigitalSource.from_supply(self.gnd, self.vcc, 0 * mAmp(tol=0))) + + self.footprint( + "X", + self.FOOTPRINT, + { + "1": self.vcc, # ST/OE pin + "2": self.gnd, + "3": self.out, + "4": self.vcc, + }, + part=self.in_kicad_part, + value=self.in_kicad_value, + datasheet=self.in_kicad_datasheet, + ) + + self.assign(self.lcsc_part, self.in_lcsc_part) + self.assign(self.actual_basic_part, self.in_actual_basic_part) class Sg8101cg_Device(Sg8101_Base_Device): - FOOTPRINT = 'Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm' # doesn't perfectly match datasheet recommended geometry + FOOTPRINT = "Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm" # doesn't perfectly match datasheet recommended geometry class Sg8101ce_Device(Sg8101_Base_Device): - FOOTPRINT = 'Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm' # doesn't perfectly match datasheet recommended geometry + FOOTPRINT = "Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm" # doesn't perfectly match datasheet recommended geometry class JlcOscillator(JlcTableSelector, TableOscillator): - SERIES_DEVICE_MAP = { - 'SG-8101CG': Sg8101cg_Device, - 'SG-8101CE': Sg8101ce_Device, - } - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("(±\S+ppm) .* (\S+MHz) .* Pre-programmed Oscillators .*"), - lambda match: { - TableOscillator.FREQUENCY: PartParserUtil.parse_abs_tolerance( - match.group(1), PartParserUtil.parse_value(match.group(2), 'Hz'), 'Hz'), - }), - ] - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] not in ('Pre-programmed Oscillators', 'Oscillators'): - return None - - footprint = None - for series, device_cls in cls.SERIES_DEVICE_MAP.items(): - if row[cls.PART_NUMBER_COL].startswith(series): - assert footprint is None, f"multiple footprint rules match {row[cls.PART_NUMBER_COL]}" - footprint = device_cls.FOOTPRINT - if footprint is None: - return None - - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row).sort_by( # TODO dedup w/ JlcTableSelector._row_sort_by - lambda row: [row[cls.BASIC_PART_HEADER], cls._row_area(row), row[cls.COST]] - ) - - @override - def _row_generate(self, row: PartsTableRow) -> None: - for series, device_cls in self.SERIES_DEVICE_MAP.items(): - if row[self.PART_NUMBER_COL].startswith(series): - self.device = self.Block(device_cls( - row[self.PART_NUMBER_COL], row[self.DESCRIPTION_COL], row[self.DATASHEET_COL], - row[self.LCSC_PART_HEADER], row[self.BASIC_PART_HEADER] == self.BASIC_PART_VALUE - )) - self.connect(self.pwr, self.device.vcc) - self.connect(self.gnd, self.device.gnd) - self.connect(self.out, self.device.out) - self.cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + SERIES_DEVICE_MAP = { + "SG-8101CG": Sg8101cg_Device, + "SG-8101CE": Sg8101ce_Device, + } + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("(±\S+ppm) .* (\S+MHz) .* Pre-programmed Oscillators .*"), + lambda match: { + TableOscillator.FREQUENCY: PartParserUtil.parse_abs_tolerance( + match.group(1), PartParserUtil.parse_value(match.group(2), "Hz"), "Hz" + ), + }, + ), + ] + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] not in ("Pre-programmed Oscillators", "Oscillators"): + return None + + footprint = None + for series, device_cls in cls.SERIES_DEVICE_MAP.items(): + if row[cls.PART_NUMBER_COL].startswith(series): + assert footprint is None, f"multiple footprint rules match {row[cls.PART_NUMBER_COL]}" + footprint = device_cls.FOOTPRINT + if footprint is None: + return None + + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return ( + cls._jlc_table() + .map_new_columns(parse_row) + .sort_by( # TODO dedup w/ JlcTableSelector._row_sort_by + lambda row: [row[cls.BASIC_PART_HEADER], cls._row_area(row), row[cls.COST]] + ) + ) + + @override + def _row_generate(self, row: PartsTableRow) -> None: + for series, device_cls in self.SERIES_DEVICE_MAP.items(): + if row[self.PART_NUMBER_COL].startswith(series): + self.device = self.Block( + device_cls( + row[self.PART_NUMBER_COL], + row[self.DESCRIPTION_COL], + row[self.DATASHEET_COL], + row[self.LCSC_PART_HEADER], + row[self.BASIC_PART_HEADER] == self.BASIC_PART_VALUE, + ) + ) + self.connect(self.pwr, self.device.vcc) + self.connect(self.gnd, self.device.gnd) + self.connect(self.out, self.device.out) + self.cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) lambda: JlcOscillator() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcPart.py b/edg/parts/JlcPart.py index e54d64918..7e13f105d 100644 --- a/edg/parts/JlcPart.py +++ b/edg/parts/JlcPart.py @@ -8,95 +8,102 @@ @non_library class JlcPart(Block): - """Provides additional data fields for JLCPCB parts for their SMT service. - By default, this does not check for basic parts, but that can be changed in refinements. - """ - def __init__(self, *args: Any, require_basic_part: BoolLike = False, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.lcsc_part = self.Parameter(StringExpr()) - self.actual_basic_part = self.Parameter(BoolExpr()) - self.require_basic_part = self.ArgParameter(require_basic_part) + """Provides additional data fields for JLCPCB parts for their SMT service. + By default, this does not check for basic parts, but that can be changed in refinements. + """ - self.require(self.require_basic_part.implies(self.actual_basic_part), "required basic part") + def __init__(self, *args: Any, require_basic_part: BoolLike = False, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.lcsc_part = self.Parameter(StringExpr()) + self.actual_basic_part = self.Parameter(BoolExpr()) + self.require_basic_part = self.ArgParameter(require_basic_part) + + self.require(self.require_basic_part.implies(self.actual_basic_part), "required basic part") + + +DescriptionParser = Tuple[re.Pattern[str], Callable[[re.Match[str]], Dict[PartsTableColumn, Any]]] -DescriptionParser = Tuple[re.Pattern[str], - Callable[[re.Match[str]], Dict[PartsTableColumn, Any]]] class JlcTableBase(PartsTableBase): - """Defines common table headers, columns, and functionality for parsing JLCPCB parts tables.""" - PART_NUMBER_COL = 'MFR.Part' # used only for translation to the PartsTableFootprint col - MANUFACTURER_COL = 'Manufacturer' - DESCRIPTION_COL = 'Description' - DATASHEET_COL = 'Datasheet' - - _PACKAGE_HEADER = 'Package' - - LCSC_PART_HEADER = 'LCSC Part' - BASIC_PART_HEADER = 'Library Type' - BASIC_PART_VALUE = 'Basic' - - COST_HEADER = 'Price' - COST = PartsTableColumn(float) - - __JLC_TABLE: Optional[PartsTable] = None - - @classmethod - def _jlc_table(cls) -> PartsTable: - """Returns the full JLC parts table, saving the result for future use.""" - if JlcTableBase.__JLC_TABLE is None: # specifically this class, so results are visible to subclasses - JlcTableBase.__JLC_TABLE = PartsTable.from_csv_files(PartsTable.with_source_dir([ - 'Pruned_JLCPCB SMT Parts Library(20220419).csv' - ], 'resources'), encoding='gb2312') - return JlcTableBase.__JLC_TABLE - - @classmethod - def _parse_jlcpcb_common(cls, row: PartsTableRow) -> Dict[PartsTableColumn, Any]: - """Returns a dict with the cost row, or errors out with KeyError.""" - # extracts out all the available prices for a given part by order quantity - price_list = re.findall(":(\d+\.\d*),", row[cls.COST_HEADER]) - float_array = [float(x) for x in price_list] - if not float_array: - cost = float('inf') # disprefer heavily if no price listed - else: - cost = max(float_array) # choose the highest price, which is for the lowest quantity - - return { - cls.COST: cost, - } - - @staticmethod - def parse(description: str, regex_dictionary: Dict[str, re.Pattern[str]]) -> Dict[str, str]: - extraction_table = {} - - for key, pattern in regex_dictionary.items(): - matches = pattern.findall(description) - if matches: # discard if not matched - assert len(matches) == 1, f"multiple matches for {pattern} for {description}" # excess matches fail noisily - assert key not in extraction_table # duplicate matches fail noisily - extraction_table[key] = matches[0] - - return extraction_table - - @staticmethod - def parse_full_description(description: str, parser_options: List[DescriptionParser]) -> \ - Optional[Dict[PartsTableColumn, Any]]: - for parser, match_fn in parser_options: - parsed_values = parser.match(description) - if parsed_values: - return match_fn(parsed_values) - - return None # exhausted all options + """Defines common table headers, columns, and functionality for parsing JLCPCB parts tables.""" + + PART_NUMBER_COL = "MFR.Part" # used only for translation to the PartsTableFootprint col + MANUFACTURER_COL = "Manufacturer" + DESCRIPTION_COL = "Description" + DATASHEET_COL = "Datasheet" + + _PACKAGE_HEADER = "Package" + + LCSC_PART_HEADER = "LCSC Part" + BASIC_PART_HEADER = "Library Type" + BASIC_PART_VALUE = "Basic" + + COST_HEADER = "Price" + COST = PartsTableColumn(float) + + __JLC_TABLE: Optional[PartsTable] = None + + @classmethod + def _jlc_table(cls) -> PartsTable: + """Returns the full JLC parts table, saving the result for future use.""" + if JlcTableBase.__JLC_TABLE is None: # specifically this class, so results are visible to subclasses + JlcTableBase.__JLC_TABLE = PartsTable.from_csv_files( + PartsTable.with_source_dir(["Pruned_JLCPCB SMT Parts Library(20220419).csv"], "resources"), + encoding="gb2312", + ) + return JlcTableBase.__JLC_TABLE + + @classmethod + def _parse_jlcpcb_common(cls, row: PartsTableRow) -> Dict[PartsTableColumn, Any]: + """Returns a dict with the cost row, or errors out with KeyError.""" + # extracts out all the available prices for a given part by order quantity + price_list = re.findall(":(\d+\.\d*),", row[cls.COST_HEADER]) + float_array = [float(x) for x in price_list] + if not float_array: + cost = float("inf") # disprefer heavily if no price listed + else: + cost = max(float_array) # choose the highest price, which is for the lowest quantity + + return { + cls.COST: cost, + } + + @staticmethod + def parse(description: str, regex_dictionary: Dict[str, re.Pattern[str]]) -> Dict[str, str]: + extraction_table = {} + + for key, pattern in regex_dictionary.items(): + matches = pattern.findall(description) + if matches: # discard if not matched + assert ( + len(matches) == 1 + ), f"multiple matches for {pattern} for {description}" # excess matches fail noisily + assert key not in extraction_table # duplicate matches fail noisily + extraction_table[key] = matches[0] + + return extraction_table + + @staticmethod + def parse_full_description( + description: str, parser_options: List[DescriptionParser] + ) -> Optional[Dict[PartsTableColumn, Any]]: + for parser, match_fn in parser_options: + parsed_values = parser.match(description) + if parsed_values: + return match_fn(parsed_values) + + return None # exhausted all options @non_library class JlcTableSelector(PartsTableAreaSelector, PartsTableFootprintFilter, JlcPart, JlcTableBase): - @classmethod - @override - def _row_sort_by(cls, row: PartsTableRow) -> Any: - return [row[cls.BASIC_PART_HEADER], cls._row_area(row), super()._row_sort_by(row), row[cls.COST]] - - @override - def _row_generate(self, row: PartsTableRow) -> None: - super()._row_generate(row) - self.assign(self.lcsc_part, row[self.LCSC_PART_HEADER]) - self.assign(self.actual_basic_part, row[self.BASIC_PART_HEADER] == self.BASIC_PART_VALUE) + @classmethod + @override + def _row_sort_by(cls, row: PartsTableRow) -> Any: + return [row[cls.BASIC_PART_HEADER], cls._row_area(row), super()._row_sort_by(row), row[cls.COST]] + + @override + def _row_generate(self, row: PartsTableRow) -> None: + super()._row_generate(row) + self.assign(self.lcsc_part, row[self.LCSC_PART_HEADER]) + self.assign(self.actual_basic_part, row[self.BASIC_PART_HEADER] == self.BASIC_PART_VALUE) diff --git a/edg/parts/JlcPptcFuse.py b/edg/parts/JlcPptcFuse.py index 5ad2c5de6..d45d327d1 100644 --- a/edg/parts/JlcPptcFuse.py +++ b/edg/parts/JlcPptcFuse.py @@ -9,50 +9,51 @@ class JlcPptcFuse(PartsTableSelectorFootprint, JlcTableSelector, TableFuse, PptcFuse): - PACKAGE_FOOTPRINT_MAP = { - '0402': 'Resistor_SMD:R_0402_1005Metric', - '0603': 'Resistor_SMD:R_0603_1608Metric', - '0805': 'Resistor_SMD:R_0805_2012Metric', - '1206': 'Resistor_SMD:R_1206_3216Metric', - '1210': 'Resistor_SMD:R_1210_3225Metric', - '1812': 'Resistor_SMD:R_1812_4532Metric', - - 'F0402': 'Resistor_SMD:R_0402_1005Metric', - 'F0603': 'Resistor_SMD:R_0603_1608Metric', - 'F0805': 'Resistor_SMD:R_0805_2012Metric', - 'F1206': 'Resistor_SMD:R_1206_3216Metric', - 'F1210': 'Resistor_SMD:R_1210_3225Metric', - 'F1812': 'Resistor_SMD:R_1812_4532Metric', - } - - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("(\S+V) (\S+A) (?:\S+A) (\S+A) .* Resettable Fuses.*"), - lambda match: { - TableFuse.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), 'V')), - TableFuse.TRIP_CURRENT: Range.exact(PartParserUtil.parse_value(match.group(2), 'A')), - TableFuse.HOLD_CURRENT: Range.exact(PartParserUtil.parse_value(match.group(3), 'A')), - }), - ] - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if not row['Second Category'] == 'Resettable Fuses': - return None - footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) - if footprint is None: - return None - - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + PACKAGE_FOOTPRINT_MAP = { + "0402": "Resistor_SMD:R_0402_1005Metric", + "0603": "Resistor_SMD:R_0603_1608Metric", + "0805": "Resistor_SMD:R_0805_2012Metric", + "1206": "Resistor_SMD:R_1206_3216Metric", + "1210": "Resistor_SMD:R_1210_3225Metric", + "1812": "Resistor_SMD:R_1812_4532Metric", + "F0402": "Resistor_SMD:R_0402_1005Metric", + "F0603": "Resistor_SMD:R_0603_1608Metric", + "F0805": "Resistor_SMD:R_0805_2012Metric", + "F1206": "Resistor_SMD:R_1206_3216Metric", + "F1210": "Resistor_SMD:R_1210_3225Metric", + "F1812": "Resistor_SMD:R_1812_4532Metric", + } + + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("(\S+V) (\S+A) (?:\S+A) (\S+A) .* Resettable Fuses.*"), + lambda match: { + TableFuse.VOLTAGE_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(1), "V")), + TableFuse.TRIP_CURRENT: Range.exact(PartParserUtil.parse_value(match.group(2), "A")), + TableFuse.HOLD_CURRENT: Range.exact(PartParserUtil.parse_value(match.group(3), "A")), + }, + ), + ] + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if not row["Second Category"] == "Resettable Fuses": + return None + footprint = cls.PACKAGE_FOOTPRINT_MAP.get(row[cls._PACKAGE_HEADER]) + if footprint is None: + return None + + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcPptcFuse() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcResistor.py b/edg/parts/JlcResistor.py index 2bbecf410..34ebe8150 100644 --- a/edg/parts/JlcResistor.py +++ b/edg/parts/JlcResistor.py @@ -8,64 +8,66 @@ class JlcResistor(PartsTableSelectorFootprint, JlcTableSelector, TableResistor): - PACKAGE_FOOTPRINT_MAP = { - # 0201 not in parts table, R_0201_0603Metric - - '0402': 'Resistor_SMD:R_0402_1005Metric', - '0603': 'Resistor_SMD:R_0603_1608Metric', - '0805': 'Resistor_SMD:R_0805_2012Metric', - '1206': 'Resistor_SMD:R_1206_3216Metric', - '2010': 'Resistor_SMD:R_2010_5025Metric', - '2512': 'Resistor_SMD:R_2512_6332Metric', - - 'R0402': 'Resistor_SMD:R_0402_1005Metric', - 'R0603': 'Resistor_SMD:R_0603_1608Metric', - 'R0805': 'Resistor_SMD:R_0805_2012Metric', - 'R1206': 'Resistor_SMD:R_1206_3216Metric', - 'R2010': 'Resistor_SMD:R_2010_5025Metric', - 'R2512': 'Resistor_SMD:R_2512_6332Metric', - } - - RESISTOR_MATCHES = { - 'resistance': re.compile("(^|\s)(\S+Ω)($|\s)"), - 'tolerance': re.compile("(^|\s)(±\S+%)($|\s)"), - 'power': re.compile("(^|\s)(\S+W)($|\s)"), - 'voltage': re.compile("(^|\s)(\S+V)($|\s)"), - } - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['First Category'] != 'Resistors': - return None - - new_cols: Dict[PartsTableColumn, Any] = {} - try: - # handle the footprint first since this is the most likely to filter - new_cols[cls.KICAD_FOOTPRINT] = cls.PACKAGE_FOOTPRINT_MAP[row[cls._PACKAGE_HEADER]] - new_cols.update(cls._parse_jlcpcb_common(row)) - - extracted_values = cls.parse(row[cls.DESCRIPTION_COL], cls.RESISTOR_MATCHES) - - new_cols[cls.RESISTANCE] = PartParserUtil.parse_abs_tolerance( - extracted_values['tolerance'][1], - PartParserUtil.parse_value(extracted_values['resistance'][1], 'Ω'), 'Ω') - - new_cols[cls.POWER_RATING] = Range.zero_to_upper( - PartParserUtil.parse_value(extracted_values['power'][1], 'W')) - - if 'voltage' in extracted_values: - new_cols[cls.VOLTAGE_RATING] = Range.zero_to_upper( - PartParserUtil.parse_value(extracted_values.get('voltage', ('', '0V'))[1], 'V')) - else: - new_cols[cls.VOLTAGE_RATING] = Range(0, 0) - - return new_cols - except (KeyError, PartParserUtil.ParseError): - return None - - return cls._jlc_table().map_new_columns(parse_row) + PACKAGE_FOOTPRINT_MAP = { + # 0201 not in parts table, R_0201_0603Metric + "0402": "Resistor_SMD:R_0402_1005Metric", + "0603": "Resistor_SMD:R_0603_1608Metric", + "0805": "Resistor_SMD:R_0805_2012Metric", + "1206": "Resistor_SMD:R_1206_3216Metric", + "2010": "Resistor_SMD:R_2010_5025Metric", + "2512": "Resistor_SMD:R_2512_6332Metric", + "R0402": "Resistor_SMD:R_0402_1005Metric", + "R0603": "Resistor_SMD:R_0603_1608Metric", + "R0805": "Resistor_SMD:R_0805_2012Metric", + "R1206": "Resistor_SMD:R_1206_3216Metric", + "R2010": "Resistor_SMD:R_2010_5025Metric", + "R2512": "Resistor_SMD:R_2512_6332Metric", + } + + RESISTOR_MATCHES = { + "resistance": re.compile("(^|\s)(\S+Ω)($|\s)"), + "tolerance": re.compile("(^|\s)(±\S+%)($|\s)"), + "power": re.compile("(^|\s)(\S+W)($|\s)"), + "voltage": re.compile("(^|\s)(\S+V)($|\s)"), + } + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["First Category"] != "Resistors": + return None + + new_cols: Dict[PartsTableColumn, Any] = {} + try: + # handle the footprint first since this is the most likely to filter + new_cols[cls.KICAD_FOOTPRINT] = cls.PACKAGE_FOOTPRINT_MAP[row[cls._PACKAGE_HEADER]] + new_cols.update(cls._parse_jlcpcb_common(row)) + + extracted_values = cls.parse(row[cls.DESCRIPTION_COL], cls.RESISTOR_MATCHES) + + new_cols[cls.RESISTANCE] = PartParserUtil.parse_abs_tolerance( + extracted_values["tolerance"][1], + PartParserUtil.parse_value(extracted_values["resistance"][1], "Ω"), + "Ω", + ) + + new_cols[cls.POWER_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(extracted_values["power"][1], "W") + ) + + if "voltage" in extracted_values: + new_cols[cls.VOLTAGE_RATING] = Range.zero_to_upper( + PartParserUtil.parse_value(extracted_values.get("voltage", ("", "0V"))[1], "V") + ) + else: + new_cols[cls.VOLTAGE_RATING] = Range(0, 0) + + return new_cols + except (KeyError, PartParserUtil.ParseError): + return None + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcResistor() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcResistorArray.py b/edg/parts/JlcResistorArray.py index 44b407c61..dd8e19e14 100644 --- a/edg/parts/JlcResistorArray.py +++ b/edg/parts/JlcResistorArray.py @@ -8,47 +8,53 @@ class JlcResistorArray(PartsTableSelectorFootprint, JlcTableSelector, TableResistorArray): - SERIES_PACKAGE_FOOTPRINT_MAP = { - ('4D03', '0603_x4'): 'Resistor_SMD:R_Array_Concave_4x0603', # 1206 overall size - ('4D03', 'RES-ARRAY-SMD'): 'Resistor_SMD:R_Array_Concave_4x0603', # same as above, but inconsistent footprint field - ('4D02', '0402_x4'): 'Resistor_SMD:R_Array_Concave_4x0402', # 2x1mm overall size - ('RC-M', '0402_x4'): 'Resistor_SMD:R_Array_Convex_4x0402', # 2x1mm overall size - } - - DESCRIPTION_PARSERS: List[DescriptionParser] = [ - (re.compile("(\S+) (±\S+%) \S+ (\S+Ω) (\S+W) ±\S+ \S+ Resistor Networks & Arrays.*"), - lambda match: { - TableResistorArray.RESISTANCE: PartParserUtil.parse_abs_tolerance( - match.group(2), PartParserUtil.parse_value(match.group(3), 'Ω'), 'Ω'), - TableResistorArray.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(4), 'W')), - TableResistorArray.COUNT: int(match.group(1)), - }), - ] - - @classmethod - @override - def _make_table(cls) -> PartsTable: - def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: - if row['Second Category'] != 'Resistor Networks & Arrays': - return None - - footprint = None - for (series, package), map_footprint in cls.SERIES_PACKAGE_FOOTPRINT_MAP.items(): - if row[cls.PART_NUMBER_COL].startswith(series) and row[cls._PACKAGE_HEADER] == package: - assert footprint is None, f"multiple footprint rules match {row[cls.PART_NUMBER_COL]}" - footprint = map_footprint - if footprint is None: - return None - - new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) - if new_cols is None: - return None - - new_cols[cls.KICAD_FOOTPRINT] = footprint - new_cols.update(cls._parse_jlcpcb_common(row)) - return new_cols - - return cls._jlc_table().map_new_columns(parse_row) + SERIES_PACKAGE_FOOTPRINT_MAP = { + ("4D03", "0603_x4"): "Resistor_SMD:R_Array_Concave_4x0603", # 1206 overall size + ( + "4D03", + "RES-ARRAY-SMD", + ): "Resistor_SMD:R_Array_Concave_4x0603", # same as above, but inconsistent footprint field + ("4D02", "0402_x4"): "Resistor_SMD:R_Array_Concave_4x0402", # 2x1mm overall size + ("RC-M", "0402_x4"): "Resistor_SMD:R_Array_Convex_4x0402", # 2x1mm overall size + } + + DESCRIPTION_PARSERS: List[DescriptionParser] = [ + ( + re.compile("(\S+) (±\S+%) \S+ (\S+Ω) (\S+W) ±\S+ \S+ Resistor Networks & Arrays.*"), + lambda match: { + TableResistorArray.RESISTANCE: PartParserUtil.parse_abs_tolerance( + match.group(2), PartParserUtil.parse_value(match.group(3), "Ω"), "Ω" + ), + TableResistorArray.POWER_RATING: Range.zero_to_upper(PartParserUtil.parse_value(match.group(4), "W")), + TableResistorArray.COUNT: int(match.group(1)), + }, + ), + ] + + @classmethod + @override + def _make_table(cls) -> PartsTable: + def parse_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]: + if row["Second Category"] != "Resistor Networks & Arrays": + return None + + footprint = None + for (series, package), map_footprint in cls.SERIES_PACKAGE_FOOTPRINT_MAP.items(): + if row[cls.PART_NUMBER_COL].startswith(series) and row[cls._PACKAGE_HEADER] == package: + assert footprint is None, f"multiple footprint rules match {row[cls.PART_NUMBER_COL]}" + footprint = map_footprint + if footprint is None: + return None + + new_cols = cls.parse_full_description(row[cls.DESCRIPTION_COL], cls.DESCRIPTION_PARSERS) + if new_cols is None: + return None + + new_cols[cls.KICAD_FOOTPRINT] = footprint + new_cols.update(cls._parse_jlcpcb_common(row)) + return new_cols + + return cls._jlc_table().map_new_columns(parse_row) lambda: JlcResistorArray() # ensure class is instantiable (non-abstract) diff --git a/edg/parts/JlcSwitches.py b/edg/parts/JlcSwitches.py index 1977693cf..a5f1719b8 100644 --- a/edg/parts/JlcSwitches.py +++ b/edg/parts/JlcSwitches.py @@ -5,35 +5,38 @@ class JlcSwitch(TactileSwitch, JlcPart, FootprintBlock): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.footprint( - 'SW', 'Button_Switch_SMD:SW_SPST_SKQG_WithoutStem', # 3.9mm x 2.9mm - { - '1': self.sw, - '2': self.com, - }, - part='5.1mm switch' - ) - self.assign(self.lcsc_part, 'C318884') - self.assign(self.actual_basic_part, True) + self.footprint( + "SW", + "Button_Switch_SMD:SW_SPST_SKQG_WithoutStem", # 3.9mm x 2.9mm + { + "1": self.sw, + "2": self.com, + }, + part="5.1mm switch", + ) + self.assign(self.lcsc_part, "C318884") + self.assign(self.actual_basic_part, True) class Skrtlae010(TactileSwitch, JlcPart, FootprintBlock): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.footprint( - 'SW', 'Button_Switch_SMD:SW_Push_1P1T-MP_NO_Horizontal_Alps_SKRTLAE010', - { - '1': self.com, - '2': self.sw, - }, - mfr='Alps Alpine', part='SKRTLAE010', - datasheet='https://www.mouser.com/datasheet/2/15/SKRT-1370725.pdf' - ) - self.assign(self.lcsc_part, 'C110293') - self.assign(self.actual_basic_part, False) + self.footprint( + "SW", + "Button_Switch_SMD:SW_Push_1P1T-MP_NO_Horizontal_Alps_SKRTLAE010", + { + "1": self.com, + "2": self.sw, + }, + mfr="Alps Alpine", + part="SKRTLAE010", + datasheet="https://www.mouser.com/datasheet/2/15/SKRT-1370725.pdf", + ) + self.assign(self.lcsc_part, "C110293") + self.assign(self.actual_basic_part, False) diff --git a/edg/parts/Joystick_Xbox.py b/edg/parts/Joystick_Xbox.py index 4ffe04098..e3656e273 100644 --- a/edg/parts/Joystick_Xbox.py +++ b/edg/parts/Joystick_Xbox.py @@ -6,13 +6,17 @@ class XboxElite2Joystick(FootprintBlock, HumanInterface): """Joystick assembly (X/Y analog axes + switch) from the XBox Elite 2 controller. Proper polarity for compatibility with hall effect sensors.""" + def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground(), [Common]) - self.pwr = self.Port(VoltageSink( - voltage_limits=(3.0, 3.6)*Volt, # assumed, for hall effect sensors - current_draw=(0, 4)*mAmp # assumed, for hall effect sensors - ), [Power]) + self.pwr = self.Port( + VoltageSink( + voltage_limits=(3.0, 3.6) * Volt, # assumed, for hall effect sensors + current_draw=(0, 4) * mAmp, # assumed, for hall effect sensors + ), + [Power], + ) self.sw = self.Port(DigitalSource.low_from_supply(self.gnd)) self.ax1 = self.Port(AnalogSource.from_supply(self.gnd, self.pwr)) self.ax2 = self.Port(AnalogSource.from_supply(self.gnd, self.pwr)) @@ -22,17 +26,16 @@ def contents(self) -> None: super().contents() self.footprint( - 'U', 'edg:Joystick_XboxElite2', + "U", + "edg:Joystick_XboxElite2", { - '1': self.sw, - '2': self.gnd, - - '3': self.gnd, - '4': self.ax1, - '5': self.pwr, - - '6': self.pwr, - '7': self.ax2, - '8': self.gnd, + "1": self.sw, + "2": self.gnd, + "3": self.gnd, + "4": self.ax1, + "5": self.pwr, + "6": self.pwr, + "7": self.ax2, + "8": self.gnd, }, ) diff --git a/edg/parts/Jumpers.py b/edg/parts/Jumpers.py index 46e378cdd..7494ceb8a 100644 --- a/edg/parts/Jumpers.py +++ b/edg/parts/Jumpers.py @@ -4,13 +4,7 @@ class SolderJumperTriangular(Jumper, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'JP', 'Jumper:SolderJumper-2_P1.3mm_Open_TrianglePad1.0x1.5mm', - { - '1': self.a, - '2': self.b - } - ) + @override + def contents(self) -> None: + super().contents() + self.footprint("JP", "Jumper:SolderJumper-2_P1.3mm_Open_TrianglePad1.0x1.5mm", {"1": self.a, "2": self.b}) diff --git a/edg/parts/Labels.py b/edg/parts/Labels.py index f27ddb87a..aecb15bd3 100644 --- a/edg/parts/Labels.py +++ b/edg/parts/Labels.py @@ -6,47 +6,31 @@ @deprecated("non-circuit footprints should be added in layout as non-schematic items") class LeadFreeIndicator(Label, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'edg:Indicator_LeadFree', - {}, - value='LeadFree' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint("U", "edg:Indicator_LeadFree", {}, value="LeadFree") @deprecated("non-circuit footprints should be added in layout as non-schematic items") class IdDots4(Label, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'edg:Indicator_IdDots_4', - {}, - value='IdDots4' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint("U", "edg:Indicator_IdDots_4", {}, value="IdDots4") @deprecated("non-circuit footprints should be added in layout as non-schematic items") class DuckLogo(Label, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'edg:Symbol_Duckling', - {}, - value='Duck' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint("U", "edg:Symbol_Duckling", {}, value="Duck") @deprecated("non-circuit footprints should be added in layout as non-schematic items") class LemurLogo(Label, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'edg:Symbol_LemurSmall', - {}, - value='Lemur' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint("U", "edg:Symbol_LemurSmall", {}, value="Lemur") diff --git a/edg/parts/Lcd_Ch280qv10_Ct.py b/edg/parts/Lcd_Ch280qv10_Ct.py index 2da5b04b5..c52140a88 100644 --- a/edg/parts/Lcd_Ch280qv10_Ct.py +++ b/edg/parts/Lcd_Ch280qv10_Ct.py @@ -8,9 +8,14 @@ class Ch280qv10_Ct_Outline(InternalSubcircuit, FootprintBlock): @override def contents(self) -> None: super().contents() - self.footprint('U', 'edg:Lcd_Ch280qv10_Ct_Outline', {}, - 'Adafruit', '2770', - datasheet='https://cdn-shop.adafruit.com/product-files/2770/SPEC-CH280QV10-CT_Rev.D.pdf') + self.footprint( + "U", + "edg:Lcd_Ch280qv10_Ct_Outline", + {}, + "Adafruit", + "2770", + datasheet="https://cdn-shop.adafruit.com/product-files/2770/SPEC-CH280QV10-CT_Rev.D.pdf", + ) class Ch280qv10_Ct_Device(InternalSubcircuit, Nonstrict3v3Compatible, Block): @@ -20,85 +25,92 @@ class Ch280qv10_Ct_Device(InternalSubcircuit, Nonstrict3v3Compatible, Block): LCD only interface is compatible with the resistive touch version. Some other IL9341 devices are 50-pin and share the digital interface but have reversed LED backlight pinning. """ + def __init__(self) -> None: super().__init__() self.conn = self.Block(Fpc050Bottom(length=50)) - gnd_pin = self.conn.pins.request('43') + gnd_pin = self.conn.pins.request("43") self.gnd = self.Export(gnd_pin.adapt_to(Ground()), [Common]) - self.connect(gnd_pin, self.conn.pins.request('48'), self.conn.pins.request('49'), self.conn.pins.request('50')) - - iovcc_pin = self.conn.pins.request('40') - self.iovcc = self.Export(iovcc_pin.adapt_to(VoltageSink.from_gnd( - self.gnd, - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (1.65, 3.6)*Volt, # extended range, abs max up to 4.6v - (1.65, 3.3)*Volt), # typ 1.8/2.8 - ))) - self.connect(iovcc_pin, self.conn.pins.request('41')) - - self.vci = self.Export(self.conn.pins.request('42').adapt_to(VoltageSink.from_gnd( - self.gnd, - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (2.5, 3.6)*Volt, # extended range, abs max up to 4.6v - (2.5, 3.3)*Volt), # typ 2.8 - ))) - - self.ledk = self.Export(self.conn.pins.request('1')) - self.leda1 = self.Export(self.conn.pins.request('2')) - self.leda2 = self.Export(self.conn.pins.request('3')) - self.leda3 = self.Export(self.conn.pins.request('4')) - self.leda4 = self.Export(self.conn.pins.request('5')) - - self.connect(gnd_pin, # per ILI9341 datasheet, fix to VDDI or VSS for pins not in use - self.conn.pins.request('11'), # VSYNC - self.conn.pins.request('12'), # HSYNC - self.conn.pins.request('13'), # DOTCLK - self.conn.pins.request('14'), # DC - ) + self.connect(gnd_pin, self.conn.pins.request("48"), self.conn.pins.request("49"), self.conn.pins.request("50")) + + iovcc_pin = self.conn.pins.request("40") + self.iovcc = self.Export( + iovcc_pin.adapt_to( + VoltageSink.from_gnd( + self.gnd, + voltage_limits=self.nonstrict_3v3_compatible.then_else( + (1.65, 3.6) * Volt, (1.65, 3.3) * Volt # extended range, abs max up to 4.6v + ), # typ 1.8/2.8 + ) + ) + ) + self.connect(iovcc_pin, self.conn.pins.request("41")) + + self.vci = self.Export( + self.conn.pins.request("42").adapt_to( + VoltageSink.from_gnd( + self.gnd, + voltage_limits=self.nonstrict_3v3_compatible.then_else( + (2.5, 3.6) * Volt, (2.5, 3.3) * Volt # extended range, abs max up to 4.6v + ), # typ 2.8 + ) + ) + ) + + self.ledk = self.Export(self.conn.pins.request("1")) + self.leda1 = self.Export(self.conn.pins.request("2")) + self.leda2 = self.Export(self.conn.pins.request("3")) + self.leda3 = self.Export(self.conn.pins.request("4")) + self.leda4 = self.Export(self.conn.pins.request("5")) + + self.connect( + gnd_pin, # per ILI9341 datasheet, fix to VDDI or VSS for pins not in use + self.conn.pins.request("11"), # VSYNC + self.conn.pins.request("12"), # HSYNC + self.conn.pins.request("13"), # DOTCLK + self.conn.pins.request("14"), # DC + ) for i in range(15, 33): # DB0-17 pins unused, must be fixed to VSS lelvel self.connect(gnd_pin, self.conn.pins.request(str(i))) - self.connect(iovcc_pin, # per ILI9341 datasheet, must be fixed to VDDI level - self.conn.pins.request('35'), # RDX - ) - - dio_model = DigitalBidir.from_supply( - self.gnd, self.iovcc, - input_threshold_factor=(0.3, 0.7) + self.connect( + iovcc_pin, # per ILI9341 datasheet, must be fixed to VDDI level + self.conn.pins.request("35"), # RDX ) + + dio_model = DigitalBidir.from_supply(self.gnd, self.iovcc, input_threshold_factor=(0.3, 0.7)) din_model = DigitalSink.from_bidir(dio_model) dout_model = DigitalSource.from_bidir(dio_model) - self.reset = self.Export(self.conn.pins.request('10').adapt_to(din_model)) + self.reset = self.Export(self.conn.pins.request("10").adapt_to(din_model)) - self.im0 = self.Export(self.conn.pins.request('6').adapt_to(din_model)) - self.im1 = self.Export(self.conn.pins.request('7').adapt_to(din_model)) - self.im2 = self.Export(self.conn.pins.request('8').adapt_to(din_model)) - self.im3 = self.Export(self.conn.pins.request('9').adapt_to(din_model)) + self.im0 = self.Export(self.conn.pins.request("6").adapt_to(din_model)) + self.im1 = self.Export(self.conn.pins.request("7").adapt_to(din_model)) + self.im2 = self.Export(self.conn.pins.request("8").adapt_to(din_model)) + self.im3 = self.Export(self.conn.pins.request("9").adapt_to(din_model)) - self.sdo = self.Export(self.conn.pins.request('33').adapt_to(dout_model)) - self.sdi = self.Export(self.conn.pins.request('34').adapt_to(din_model)) - self.wr_rs = self.Export(self.conn.pins.request('36').adapt_to(din_model)) - self.rs_scl = self.Export(self.conn.pins.request('37').adapt_to(din_model)) - self.cs = self.Export(self.conn.pins.request('38').adapt_to(din_model)) + self.sdo = self.Export(self.conn.pins.request("33").adapt_to(dout_model)) + self.sdi = self.Export(self.conn.pins.request("34").adapt_to(din_model)) + self.wr_rs = self.Export(self.conn.pins.request("36").adapt_to(din_model)) + self.rs_scl = self.Export(self.conn.pins.request("37").adapt_to(din_model)) + self.cs = self.Export(self.conn.pins.request("38").adapt_to(din_model)) # pin 39 TE out unused - ctp_dio_model = DigitalBidir.from_supply( - self.gnd, self.iovcc, - input_threshold_abs=(1.0, 1.9)*Volt - ) + ctp_dio_model = DigitalBidir.from_supply(self.gnd, self.iovcc, input_threshold_abs=(1.0, 1.9) * Volt) self.ctp_i2c = self.Port(I2cTarget(DigitalBidir.empty(), [0x38]), optional=True) - self.connect(self.conn.pins.request('44').adapt_to(din_model), self.ctp_i2c.scl) - self.connect(self.conn.pins.request('45').adapt_to(dio_model), self.ctp_i2c.sda) + self.connect(self.conn.pins.request("44").adapt_to(din_model), self.ctp_i2c.scl) + self.connect(self.conn.pins.request("45").adapt_to(dio_model), self.ctp_i2c.sda) # pin 46 is CTQ IRQ, unused (semantics not defined) - self.ctp_res = self.Export(self.conn.pins.request('47').adapt_to(DigitalSink.from_bidir(ctp_dio_model))) + self.ctp_res = self.Export(self.conn.pins.request("47").adapt_to(DigitalSink.from_bidir(ctp_dio_model))) self.require(self.ctp_i2c.is_connected().implies(self.ctp_res.is_connected())) class Ch280qv10_Ct(Lcd, Resettable, Block): """ILI9341-based 2.8" 320x240 color TFT supporting SPI interface and with optional capacitive touch - Based on this example design https://www.adafruit.com/product/1947 / https://learn.adafruit.com/adafruit-2-8-tft-touch-shield-v2/downloads""" + Based on this example design https://www.adafruit.com/product/1947 / https://learn.adafruit.com/adafruit-2-8-tft-touch-shield-v2/downloads + """ + def __init__(self) -> None: super().__init__() self.device = self.Block(Ch280qv10_Ct_Device()) @@ -132,9 +144,14 @@ def contents(self) -> None: self.connect(self.device.ledk.adapt_to(Ground()), self.gnd) self.led_res = ElementDict[Resistor]() - for i, leda in [('1', self.device.leda1), ('2', self.device.leda2), - ('3', self.device.leda3), ('4', self.device.leda4)]: + for i, leda in [ + ("1", self.device.leda1), + ("2", self.device.leda2), + ("3", self.device.leda3), + ("4", self.device.leda4), + ]: led_res = self.led_res[i] = self.Block( # TODO dynamic LED resistance, this is sized for 5v - Resistor(47*Ohm(tol=0.05), power=self.pwr.link().voltage * self.pwr.link().voltage / 47)) - self.connect(led_res.a.adapt_to(VoltageSink(current_draw=(0, 80)*mAmp)), self.pwr) # TODO better current + Resistor(47 * Ohm(tol=0.05), power=self.pwr.link().voltage * self.pwr.link().voltage / 47) + ) + self.connect(led_res.a.adapt_to(VoltageSink(current_draw=(0, 80) * mAmp)), self.pwr) # TODO better current self.connect(led_res.b, leda) diff --git a/edg/parts/Lcd_Er_Tft1_28_3.py b/edg/parts/Lcd_Er_Tft1_28_3.py index 3734b8e16..4a3299807 100644 --- a/edg/parts/Lcd_Er_Tft1_28_3.py +++ b/edg/parts/Lcd_Er_Tft1_28_3.py @@ -10,9 +10,14 @@ class Er_Tft_128_3_Outline(InternalSubcircuit, FootprintBlock): @override def contents(self) -> None: super().contents() - self.footprint('U', 'edg:Lcd_Er_Tft1_28_3_Outline', {}, - 'EastRising', 'ER-TFT1.28-3', - datasheet='https://www.buydisplay.com/download/manual/ER-TFT1.28-3_Datasheet.pdf') + self.footprint( + "U", + "edg:Lcd_Er_Tft1_28_3_Outline", + {}, + "EastRising", + "ER-TFT1.28-3", + datasheet="https://www.buydisplay.com/download/manual/ER-TFT1.28-3_Datasheet.pdf", + ) class Er_Tft_128_3_Device(InternalSubcircuit, Nonstrict3v3Compatible, Block): @@ -21,53 +26,58 @@ class Er_Tft_128_3_Device(InternalSubcircuit, Nonstrict3v3Compatible, Block): def __init__(self) -> None: super().__init__() - self.conn = self.Block(Fpc050Bottom(length=15)) # Pin numbering in the doc is flipped in the footprint + self.conn = self.Block(Fpc050Bottom(length=15)) # Pin numbering in the doc is flipped in the footprint # Power pins - self.vdd = self.Export(self.conn.pins.request('12').adapt_to(VoltageSink( - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (2.5, 3.6) * Volt, # abs max is 4.6v - (2.5, 3.3) * Volt), - ))) + self.vdd = self.Export( + self.conn.pins.request("12").adapt_to( + VoltageSink( + voltage_limits=self.nonstrict_3v3_compatible.then_else( + (2.5, 3.6) * Volt, (2.5, 3.3) * Volt # abs max is 4.6v + ), + ) + ) + ) - self.gnd = self.Export(self.conn.pins.request('15').adapt_to(Ground())) + self.gnd = self.Export(self.conn.pins.request("15").adapt_to(Ground())) # Backlight control - self.ledk = self.Export(self.conn.pins.request('14')) - self.leda = self.Export(self.conn.pins.request('13')) + self.ledk = self.Export(self.conn.pins.request("14")) + self.leda = self.Export(self.conn.pins.request("13")) dio_model = DigitalBidir.from_supply( - self.gnd, self.vdd, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - input_threshold_factor=(0.3, 0.7) + self.gnd, self.vdd, voltage_limit_tolerance=(-0.3, 0.3) * Volt, input_threshold_factor=(0.3, 0.7) ) - self.rs = self.Export(self.conn.pins.request('11').adapt_to(DigitalSink.from_bidir(dio_model))) + self.rs = self.Export(self.conn.pins.request("11").adapt_to(DigitalSink.from_bidir(dio_model))) # Control pins self.spi = self.Port(SpiPeripheral.empty()) - self.cs = self.Export(self.conn.pins.request('10').adapt_to(DigitalSink.from_bidir(dio_model))) - self.connect(self.spi.sck, self.conn.pins.request('9').adapt_to(DigitalSink.from_bidir(dio_model))) - self.connect(self.spi.mosi, self.conn.pins.request('8').adapt_to(DigitalSink.from_bidir(dio_model))) + self.cs = self.Export(self.conn.pins.request("10").adapt_to(DigitalSink.from_bidir(dio_model))) + self.connect(self.spi.sck, self.conn.pins.request("9").adapt_to(DigitalSink.from_bidir(dio_model))) + self.connect(self.spi.mosi, self.conn.pins.request("8").adapt_to(DigitalSink.from_bidir(dio_model))) self.miso_nc = self.Block(DigitalBidirNotConnected()) self.connect(self.spi.miso, self.miso_nc.port) - self.rst = self.Export(self.conn.pins.request('7').adapt_to(DigitalSink.from_bidir(dio_model))) + self.rst = self.Export(self.conn.pins.request("7").adapt_to(DigitalSink.from_bidir(dio_model))) # Capacitive Touch Panel (CTP) - self.ctp_i2c = self.Port(I2cTarget(DigitalBidir.empty(), addresses=[0x15]),) + self.ctp_i2c = self.Port( + I2cTarget(DigitalBidir.empty(), addresses=[0x15]), + ) - self.ctp_vdd = self.Export(self.conn.pins.request('6').adapt_to(VoltageSink( - voltage_limits=(2.7, 3.6) * Volt, - current_draw=(5 * uAmp, 2.5 * mAmp) - ))) + self.ctp_vdd = self.Export( + self.conn.pins.request("6").adapt_to( + VoltageSink(voltage_limits=(2.7, 3.6) * Volt, current_draw=(5 * uAmp, 2.5 * mAmp)) + ) + ) - self.connect(self.gnd, self.conn.pins.request('5').adapt_to(Ground())) + self.connect(self.gnd, self.conn.pins.request("5").adapt_to(Ground())) - self.ctp_rst = self.Export(self.conn.pins.request('4').adapt_to(DigitalSink.from_bidir(dio_model))) - self.ctp_int = self.Export(self.conn.pins.request('3').adapt_to(DigitalSink.from_bidir(dio_model))) - self.connect(self.ctp_i2c.sda, self.conn.pins.request('2').adapt_to(dio_model)) - self.connect(self.ctp_i2c.scl, self.conn.pins.request('1').adapt_to(DigitalSink.from_bidir(dio_model))) + self.ctp_rst = self.Export(self.conn.pins.request("4").adapt_to(DigitalSink.from_bidir(dio_model))) + self.ctp_int = self.Export(self.conn.pins.request("3").adapt_to(DigitalSink.from_bidir(dio_model))) + self.connect(self.ctp_i2c.sda, self.conn.pins.request("2").adapt_to(dio_model)) + self.connect(self.ctp_i2c.scl, self.conn.pins.request("1").adapt_to(DigitalSink.from_bidir(dio_model))) class Er_Tft_128_3(Lcd, Resettable, Block): @@ -82,9 +92,9 @@ def __init__(self) -> None: self.cs = self.Export(self.ic.cs) self.dc = self.Export(self.ic.rs) # touch interface - self.ctp_i2c = self.Export(self.ic.ctp_i2c, optional=True, doc='Touch panel interface i2c') - self.ctp_rst = self.Export(self.ic.ctp_rst, optional=True, doc='Touch panel interface') - self.ctp_int = self.Export(self.ic.ctp_int, optional=True, doc='Touch panel interface') + self.ctp_i2c = self.Export(self.ic.ctp_i2c, optional=True, doc="Touch panel interface i2c") + self.ctp_rst = self.Export(self.ic.ctp_rst, optional=True, doc="Touch panel interface") + self.ctp_int = self.Export(self.ic.ctp_int, optional=True, doc="Touch panel interface") @override def contents(self) -> None: @@ -95,14 +105,18 @@ def contents(self) -> None: self.lcd = self.Block(Er_Tft_128_3_Outline()) # for ic outline self.connect(self.ic.ledk.adapt_to(Ground()), self.gnd) - forward_current = (24, 30)*mAmp - forward_voltage = 2.9*Volt - self.led_res = self.Block(Resistor( - resistance=((self.pwr.link().voltage.upper() - forward_voltage) / forward_current.upper(), - (self.pwr.link().voltage.lower() - forward_voltage) / forward_current.lower()))) + forward_current = (24, 30) * mAmp + forward_voltage = 2.9 * Volt + self.led_res = self.Block( + Resistor( + resistance=( + (self.pwr.link().voltage.upper() - forward_voltage) / forward_current.upper(), + (self.pwr.link().voltage.lower() - forward_voltage) / forward_current.lower(), + ) + ) + ) self.connect(self.led_res.a.adapt_to(VoltageSink(current_draw=forward_current)), self.pwr) self.connect(self.led_res.b, self.ic.leda) self.connect(self.pwr, self.ic.ctp_vdd) self.require(self.ctp_i2c.is_connected() == self.ctp_rst.is_connected()) self.require(self.ctp_i2c.is_connected() == self.ctp_int.is_connected()) - diff --git a/edg/parts/Lcd_Qt096t_if09.py b/edg/parts/Lcd_Qt096t_if09.py index 16a2105f6..d6fbc4c00 100644 --- a/edg/parts/Lcd_Qt096t_if09.py +++ b/edg/parts/Lcd_Qt096t_if09.py @@ -5,64 +5,75 @@ class Qt096t_if09_Device(InternalSubcircuit, Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.conn = self.Block(Fpc050Bottom(length=8)) + self.conn = self.Block(Fpc050Bottom(length=8)) - # both Vdd and VddI - self.vdd = self.Export(self.conn.pins.request('7').adapt_to(VoltageSink( - voltage_limits=(2.5, 4.8) * Volt, # 2.75v typ - current_draw=(0.02, 2.02) * mAmp # ST7735S Table 7.3, IDDI + IDD, typ - max - ))) - self.gnd = self.Export(self.conn.pins.request('2').adapt_to(Ground())) + # both Vdd and VddI + self.vdd = self.Export( + self.conn.pins.request("7").adapt_to( + VoltageSink( + voltage_limits=(2.5, 4.8) * Volt, # 2.75v typ + current_draw=(0.02, 2.02) * mAmp, # ST7735S Table 7.3, IDDI + IDD, typ - max + ) + ) + ) + self.gnd = self.Export(self.conn.pins.request("2").adapt_to(Ground())) - io_model = DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - current_draw=0*mAmp(tol=0), - input_threshold_factor=(0.3, 0.7), - ) - self.reset = self.Export(self.conn.pins.request('3').adapt_to(io_model)) - # data / command selection pin - self.rs = self.Export(self.conn.pins.request('4').adapt_to(io_model)) - self.cs = self.Export(self.conn.pins.request('8').adapt_to(io_model)) + io_model = DigitalSink.from_supply( + self.gnd, + self.vdd, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_draw=0 * mAmp(tol=0), + input_threshold_factor=(0.3, 0.7), + ) + self.reset = self.Export(self.conn.pins.request("3").adapt_to(io_model)) + # data / command selection pin + self.rs = self.Export(self.conn.pins.request("4").adapt_to(io_model)) + self.cs = self.Export(self.conn.pins.request("8").adapt_to(io_model)) - self.spi = self.Port(SpiPeripheral.empty()) - self.connect(self.spi.sck, self.conn.pins.request('6').adapt_to(io_model)) # scl - self.connect(self.spi.mosi, self.conn.pins.request('5').adapt_to(io_model)) # sda + self.spi = self.Port(SpiPeripheral.empty()) + self.connect(self.spi.sck, self.conn.pins.request("6").adapt_to(io_model)) # scl + self.connect(self.spi.mosi, self.conn.pins.request("5").adapt_to(io_model)) # sda - self.miso_nc = self.Block(DigitalBidirNotConnected()) - self.connect(self.spi.miso, self.miso_nc.port) + self.miso_nc = self.Block(DigitalBidirNotConnected()) + self.connect(self.spi.miso, self.miso_nc.port) - self.leda = self.Export(self.conn.pins.request('1')) # TODO maybe something else? + self.leda = self.Export(self.conn.pins.request("1")) # TODO maybe something else? class Qt096t_if09(Lcd, Resettable, Block): - """ST7735S-based LCD module with a 8-pin 0.5mm-pitch FPC connector""" - def __init__(self) -> None: - super().__init__() - - self.device = self.Block(Qt096t_if09_Device()) - self.gnd = self.Export(self.device.gnd, [Common]) - self.pwr = self.Export(self.device.vdd, [Power]) - self.rs = self.Export(self.device.rs) - self.cs = self.Export(self.device.cs) - self.spi = self.Export(self.device.spi) - self.led = self.Port(DigitalSink.empty()) - - @override - def contents(self) -> None: - super().contents() - self.connect(self.reset, self.device.reset) - self.require(self.reset.is_connected()) - - self.led_res = self.Block(Resistor(resistance=100*Ohm(tol=0.05))) # TODO dynamic sizing, power - self.connect(self.led_res.a.adapt_to(DigitalSink( - # no voltage limits, since the resistor is autogen'd - input_thresholds=(3, 3), # TODO more accurate model - current_draw=(16, 20) * mAmp # TODO user-configurable? - )), self.led) - self.connect(self.led_res.b, self.device.leda) - - self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + """ST7735S-based LCD module with a 8-pin 0.5mm-pitch FPC connector""" + + def __init__(self) -> None: + super().__init__() + + self.device = self.Block(Qt096t_if09_Device()) + self.gnd = self.Export(self.device.gnd, [Common]) + self.pwr = self.Export(self.device.vdd, [Power]) + self.rs = self.Export(self.device.rs) + self.cs = self.Export(self.device.cs) + self.spi = self.Export(self.device.spi) + self.led = self.Port(DigitalSink.empty()) + + @override + def contents(self) -> None: + super().contents() + self.connect(self.reset, self.device.reset) + self.require(self.reset.is_connected()) + + self.led_res = self.Block(Resistor(resistance=100 * Ohm(tol=0.05))) # TODO dynamic sizing, power + self.connect( + self.led_res.a.adapt_to( + DigitalSink( + # no voltage limits, since the resistor is autogen'd + input_thresholds=(3, 3), # TODO more accurate model + current_draw=(16, 20) * mAmp, # TODO user-configurable? + ) + ), + self.led, + ) + self.connect(self.led_res.b, self.device.leda) + + self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/LedDriver_Al8861.py b/edg/parts/LedDriver_Al8861.py index 7b5d6f155..978de9804 100644 --- a/edg/parts/LedDriver_Al8861.py +++ b/edg/parts/LedDriver_Al8861.py @@ -8,47 +8,52 @@ class Al8861_Device(InternalSubcircuit, JlcPart, FootprintBlock): def __init__(self, peak_output_current: FloatLike): super().__init__() - self.vin = self.Port(VoltageSink( - voltage_limits=(4.5, 40)*Volt, - current_draw=(0.055, 0.55)*mAmp, # shutdown typ to quiescent typ - ), [Power]) + self.vin = self.Port( + VoltageSink( + voltage_limits=(4.5, 40) * Volt, + current_draw=(0.055, 0.55) * mAmp, # shutdown typ to quiescent typ + ), + [Power], + ) self.gnd = self.Port(Ground(), [Common]) self.lx = self.Port(Passive()) self.isense = self.Port(AnalogSink()) - self.vset_pwm = self.Port(DigitalSink( - voltage_limits=(0, 5)*Volt, - input_thresholds=(0.2, 0.25)*Volt - ), optional=True) + self.vset_pwm = self.Port( + DigitalSink(voltage_limits=(0, 5) * Volt, input_thresholds=(0.2, 0.25) * Volt), optional=True + ) self.peak_output_current = self.ArgParameter(peak_output_current) @override def contents(self) -> None: super().contents() - self.require(self.peak_output_current < 2*Amp) + self.require(self.peak_output_current < 2 * Amp) self.footprint( - 'U', 'Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.68x1.88mm_ThermalVias', + "U", + "Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.68x1.88mm_ThermalVias", { - '1': self.isense, - '2': self.gnd, - '3': self.gnd, - '4': self.vset_pwm, - '5': self.lx, - '6': self.lx, + "1": self.isense, + "2": self.gnd, + "3": self.gnd, + "4": self.vset_pwm, + "5": self.lx, + "6": self.lx, # '7': NC - '8': self.vin, + "8": self.vin, # '9': EP, connectivity not specified }, - mfr='Diodes Incorporated', part='AL8861MP', - datasheet='https://www.diodes.com/assets/Datasheets/AL8861.pdf' + mfr="Diodes Incorporated", + part="AL8861MP", + datasheet="https://www.diodes.com/assets/Datasheets/AL8861.pdf", ) - self.assign(self.lcsc_part, 'C155534') + self.assign(self.lcsc_part, "C155534") self.assign(self.actual_basic_part, False) class Al8861(LedDriverPwm, LedDriverSwitchingConverter, LedDriver, GeneratorBlock): """AL8861 buck LED driver.""" + def __init__(self, diode_voltage_drop: RangeLike = Range.all()): super().__init__() @@ -68,39 +73,41 @@ def generate(self) -> None: super().generate() # TODO replace with BuckConverterPowerPath, though the 33uH minimum inductance is very high - self.require(self.max_current.within((0, 1.5)*Amp)) # for MSOP and SOT89 packages + self.require(self.max_current.within((0, 1.5) * Amp)) # for MSOP and SOT89 packages isense_ref = Range(0.096, 0.104) - self.rsense = self.Block(CurrentSenseResistor(resistance=(1/self.max_current).shrink_multiply(isense_ref), - sense_in_reqd=False)) + self.rsense = self.Block( + CurrentSenseResistor(resistance=(1 / self.max_current).shrink_multiply(isense_ref), sense_in_reqd=False) + ) self.connect(self.rsense.pwr_in, self.pwr) self.connect(self.rsense.sense_out, self.ic.isense) self.connect(self.rsense.pwr_out, self.leda.adapt_to(VoltageSink(current_draw=self.max_current))) - self.pwr_cap = self.Block(DecouplingCapacitor((4.7*0.8, 10*1.2)*uFarad))\ - .connected(self.gnd, self.pwr) # "commonly used values"" + self.pwr_cap = self.Block(DecouplingCapacitor((4.7 * 0.8, 10 * 1.2) * uFarad)).connected( + self.gnd, self.pwr + ) # "commonly used values"" peak_current = isense_ref / self.rsense.actual_resistance + (0, self.ripple_limit / 2) self.assign(self.ic.peak_output_current, peak_current.upper()) # minimum switch on time = 500ns, recommended maximum switch off time = 250kHz @ 75% = 30us - min_inductance = self.pwr.link().voltage.upper() * (1.5*uSecond) / self.ripple_limit + min_inductance = self.pwr.link().voltage.upper() * (1.5 * uSecond) / self.ripple_limit - self.ind = self.Block(Inductor( - inductance=(min_inductance, float('inf')), - current=peak_current, - frequency=(0.25, 1)*MHertz - )) + self.ind = self.Block( + Inductor(inductance=(min_inductance, float("inf")), current=peak_current, frequency=(0.25, 1) * MHertz) + ) # recommended inductance between 33 and 100uH, accounting for ~20% tolerance - self.require(self.ind.actual_inductance.within((33*0.8, 100*1.2)*uHenry)) - self.assign(self.actual_ripple, self.pwr.link().voltage * (1.5*uSecond) / self.ind.actual_inductance) - - self.diode = self.Block(Diode( - reverse_voltage=self.pwr.link().voltage, - current=peak_current, - voltage_drop=self.diode_voltage_drop, - reverse_recovery_time=(0, 500)*nSecond, # arbitrary for 'fast' - )) + self.require(self.ind.actual_inductance.within((33 * 0.8, 100 * 1.2) * uHenry)) + self.assign(self.actual_ripple, self.pwr.link().voltage * (1.5 * uSecond) / self.ind.actual_inductance) + + self.diode = self.Block( + Diode( + reverse_voltage=self.pwr.link().voltage, + current=peak_current, + voltage_drop=self.diode_voltage_drop, + reverse_recovery_time=(0, 500) * nSecond, # arbitrary for 'fast' + ) + ) self.connect(self.ind.a, self.ledk) self.connect(self.ind.b, self.ic.lx, self.diode.anode) self.connect(self.diode.cathode.adapt_to(VoltageSink()), self.pwr) diff --git a/edg/parts/LedDriver_Tps92200.py b/edg/parts/LedDriver_Tps92200.py index bbd8ca922..a194a6f47 100644 --- a/edg/parts/LedDriver_Tps92200.py +++ b/edg/parts/LedDriver_Tps92200.py @@ -9,20 +9,20 @@ def __init__(self, peak_output_current: FloatLike): super().__init__() self.gnd = self.Port(Ground(), [Common]) - self.vin = self.Port(VoltageSink( - voltage_limits=(4, 30)*Volt, # note, UVLO at 3.5-3.9v - current_draw=(0.001, 1)*mAmp, # shutdown typ (Vdim=0) to operating max - ), [Power]) - - self.dim = self.Port(DigitalSink.from_supply( - self.gnd, self.vin, - voltage_limit_abs=(-0.1, 6)*Volt, - input_threshold_abs=(0.3, 0.65)*Volt - )) - self.fb = self.Port(AnalogSink.from_supply( - self.gnd, self.vin, - voltage_limit_abs=(-0.1, 6)*Volt - )) + self.vin = self.Port( + VoltageSink( + voltage_limits=(4, 30) * Volt, # note, UVLO at 3.5-3.9v + current_draw=(0.001, 1) * mAmp, # shutdown typ (Vdim=0) to operating max + ), + [Power], + ) + + self.dim = self.Port( + DigitalSink.from_supply( + self.gnd, self.vin, voltage_limit_abs=(-0.1, 6) * Volt, input_threshold_abs=(0.3, 0.65) * Volt + ) + ) + self.fb = self.Port(AnalogSink.from_supply(self.gnd, self.vin, voltage_limit_abs=(-0.1, 6) * Volt)) self.sw = self.Port(VoltageSource()) self.boot = self.Port(Passive()) @@ -31,28 +31,35 @@ def __init__(self, peak_output_current: FloatLike): def contents(self) -> None: super().contents() self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', + "U", + "Package_TO_SOT_SMD:SOT-23-6", { - '6': self.boot, - '1': self.fb, - '3': self.gnd, - '2': self.dim, - '5': self.sw, - '4': self.vin, + "6": self.boot, + "1": self.fb, + "3": self.gnd, + "2": self.dim, + "5": self.sw, + "4": self.vin, }, - mfr='Texas Instruments', part='TPS92200D2DDCR', - datasheet='https://www.ti.com/lit/ds/symlink/tps92200.pdf' + mfr="Texas Instruments", + part="TPS92200D2DDCR", + datasheet="https://www.ti.com/lit/ds/symlink/tps92200.pdf", ) - self.assign(self.lcsc_part, 'C2865497') + self.assign(self.lcsc_part, "C2865497") self.assign(self.actual_basic_part, False) class Tps92200(LedDriverPwm, LedDriver, GeneratorBlock): """TPS92200 buck 4-30V 1.5A 1 MHz LED driver and 150nS min on-time. This is the -D2 variant, with PWM input for 1-100% range as a 20-200kHz digital signal""" - def __init__(self, led_voltage: RangeLike = (1, 4)*Volt, *, - input_ripple_limit: FloatLike = 0.2*Volt, # from 8.2 example application - output_ripple_limit: FloatLike = 0.01*Volt) -> None: + + def __init__( + self, + led_voltage: RangeLike = (1, 4) * Volt, + *, + input_ripple_limit: FloatLike = 0.2 * Volt, # from 8.2 example application + output_ripple_limit: FloatLike = 0.01 * Volt, + ) -> None: super().__init__() self.ic = self.Block(Tps92200_Device(FloatExpr())) @@ -70,31 +77,37 @@ def generate(self) -> None: super().generate() with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.pwr, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: - self.cap = imp.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2))) + self.cap = imp.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))) - self.boot_cap = self.Block(Capacitor(capacitance=0.1*uFarad(tol=0.2), voltage=(0, 7) * Volt)) + self.boot_cap = self.Block(Capacitor(capacitance=0.1 * uFarad(tol=0.2), voltage=(0, 7) * Volt)) self.connect(self.boot_cap.neg.adapt_to(VoltageSink()), self.ic.sw) self.connect(self.boot_cap.pos, self.ic.boot) isense_ref = Range(0.096, 0.102) - self.rsense = self.Block(CurrentSenseResistor(resistance=(1/self.max_current).shrink_multiply(isense_ref), - sense_in_reqd=False)) + self.rsense = self.Block( + CurrentSenseResistor(resistance=(1 / self.max_current).shrink_multiply(isense_ref), sense_in_reqd=False) + ) self.connect(self.rsense.sense_out, self.ic.fb) self.connect(self.rsense.pwr_out, self.ledk.adapt_to(VoltageSink(current_draw=self.max_current))) self.connect(self.rsense.pwr_in, self.gnd.as_voltage_source()) - self.require(self.pwr.current_draw.within((0, 1.5)*Amp)) # continuous current rating - frequency = (0.8, 1.2)*MHertz - self.power_path = imp.Block(BuckConverterPowerPath( - self.pwr.link().voltage, self.led_voltage, frequency, - self.max_current, (0, 2.4)*Amp, # lower of switch source limits - input_voltage_ripple=self.input_ripple_limit, - output_voltage_ripple=self.output_ripple_limit, - dutycycle_limit=(0, 0.99) - )) + self.require(self.pwr.current_draw.within((0, 1.5) * Amp)) # continuous current rating + frequency = (0.8, 1.2) * MHertz + self.power_path = imp.Block( + BuckConverterPowerPath( + self.pwr.link().voltage, + self.led_voltage, + frequency, + self.max_current, + (0, 2.4) * Amp, # lower of switch source limits + input_voltage_ripple=self.input_ripple_limit, + output_voltage_ripple=self.output_ripple_limit, + dutycycle_limit=(0, 0.99), + ) + ) self.connect(self.power_path.pwr_out, self.leda.adapt_to(VoltageSink(current_draw=self.max_current))) self.connect(self.power_path.switch, self.ic.sw) - self.require(self.power_path.actual_inductor_current_ripple.lower() > 0.3*Amp) # minimum for stability + self.require(self.power_path.actual_inductor_current_ripple.lower() > 0.3 * Amp) # minimum for stability diff --git a/edg/parts/LedMatrix.py b/edg/parts/LedMatrix.py index bc9641b9b..9aa1135d2 100644 --- a/edg/parts/LedMatrix.py +++ b/edg/parts/LedMatrix.py @@ -5,32 +5,33 @@ class CharlieplexedLedMatrix(Light, GeneratorBlock, SvgPcbTemplateBlock): - """A LED matrix that saves on IO pins by charlieplexing, only requiring max(rows + 1, cols) GPIOs to control. - Requires IOs that can tri-state, and requires scanning through rows (so not all LEDs are simultaneously on). - - Anodes (columns) are directly connected to the IO line, while the cathodes (rows) are connected through a resistor. - A generalization of https://en.wikipedia.org/wiki/Charlieplexing#/media/File:3-pin_Charlieplexing_matrix_with_common_resistors.svg - """ - @override - def _svgpcb_fn_name_adds(self) -> Optional[str]: - return f"{self._svgpcb_get(self.ncols)}_{self._svgpcb_get(self.nrows)}" - - @override - def _svgpcb_template(self) -> str: - led_block = self._svgpcb_footprint_block_path_of(['led[0_0]']) - res_block = self._svgpcb_footprint_block_path_of(['res[0]']) - led_reftype, led_refnum = self._svgpcb_refdes_of(['led[0_0]']) - res_reftype, res_refnum = self._svgpcb_refdes_of(['res[0]']) - assert led_block is not None and res_block is not None - led_footprint = self._svgpcb_footprint_of(led_block) - led_a_pin = self._svgpcb_pin_of(['led[0_0]'], ['a']) - led_k_pin = self._svgpcb_pin_of(['led[0_0]'], ['k']) - res_footprint = self._svgpcb_footprint_of(res_block) - res_a_pin = self._svgpcb_pin_of(['res[0]'], ['a']) - res_b_pin = self._svgpcb_pin_of(['res[0]'], ['b']) - assert all([pin is not None for pin in [led_a_pin, led_k_pin, res_a_pin, res_b_pin]]) - - return f"""\ + """A LED matrix that saves on IO pins by charlieplexing, only requiring max(rows + 1, cols) GPIOs to control. + Requires IOs that can tri-state, and requires scanning through rows (so not all LEDs are simultaneously on). + + Anodes (columns) are directly connected to the IO line, while the cathodes (rows) are connected through a resistor. + A generalization of https://en.wikipedia.org/wiki/Charlieplexing#/media/File:3-pin_Charlieplexing_matrix_with_common_resistors.svg + """ + + @override + def _svgpcb_fn_name_adds(self) -> Optional[str]: + return f"{self._svgpcb_get(self.ncols)}_{self._svgpcb_get(self.nrows)}" + + @override + def _svgpcb_template(self) -> str: + led_block = self._svgpcb_footprint_block_path_of(["led[0_0]"]) + res_block = self._svgpcb_footprint_block_path_of(["res[0]"]) + led_reftype, led_refnum = self._svgpcb_refdes_of(["led[0_0]"]) + res_reftype, res_refnum = self._svgpcb_refdes_of(["res[0]"]) + assert led_block is not None and res_block is not None + led_footprint = self._svgpcb_footprint_of(led_block) + led_a_pin = self._svgpcb_pin_of(["led[0_0]"], ["a"]) + led_k_pin = self._svgpcb_pin_of(["led[0_0]"], ["k"]) + res_footprint = self._svgpcb_footprint_of(res_block) + res_a_pin = self._svgpcb_pin_of(["res[0]"], ["a"]) + res_b_pin = self._svgpcb_pin_of(["res[0]"], ["b"]) + assert all([pin is not None for pin in [led_a_pin, led_k_pin, res_a_pin, res_b_pin]]) + + return f"""\ function {self._svgpcb_fn_name()}(xy, colSpacing=0.2, rowSpacing=0.2) {{ const kXCount = {self._svgpcb_get(self.ncols)} // number of columns (x dimension) const kYCount = {self._svgpcb_get(self.nrows)} // number of rows (y dimension) @@ -164,80 +165,87 @@ def _svgpcb_template(self) -> str: }} """ - @override - def _svgpcb_bbox(self) -> Tuple[float, float, float, float]: - return (-1.0, -1.0, - self._svgpcb_get(self.ncols) * .2 * 25.4 + 1.0, (self._svgpcb_get(self.nrows) + 1) * .2 * 25.4 + 1.0) - - def __init__(self, nrows: IntLike, ncols: IntLike, - color: LedColorLike = Led.Any, current_draw: RangeLike = (1, 10)*mAmp): - super().__init__() - - self.current_draw = self.ArgParameter(current_draw) - self.color = self.ArgParameter(color) - - # note that IOs supply both the positive and negative - self.ios = self.Port(Vector(DigitalSink.empty())) - - self.nrows = self.ArgParameter(nrows) - self.ncols = self.ArgParameter(ncols) - self.generator_param(self.nrows, self.ncols) - - @override - def generate(self) -> None: - super().generate() - nrows = self.get(self.nrows) - ncols = self.get(self.ncols) - - io_voltage = self.ios.hull(lambda x: x.link().voltage) - io_voltage_upper = io_voltage.upper() - io_voltage_lower = self.ios.hull(lambda x: x.link().output_thresholds).upper() - - # internally, this uses passive ports on all the components, and only casts to a DigitalSink at the end - # which is necessary to account for that not all LEDs can be simultaneously on - passive_ios: Dict[int, Passive] = {} # keeps the passive-side port for each boundary IO - def connect_passive_io(index: int, io: Passive) -> None: - # connects a Passive-typed IO to the index, handling the first and subsequent case - if index in passive_ios: - self.connect(passive_ios[index], io) # subsequent case, actually do the connection - else: - passive_ios[index] = io # first case, just bootstrap the data structure - - self.res = ElementDict[Resistor]() - res_model = Resistor( - resistance=(io_voltage_upper / self.current_draw.upper(), - io_voltage_lower / self.current_draw.lower()) - ) - self.led = ElementDict[Led]() - led_model = Led(color=self.color) - - # generate the resistor and LEDs for each column - for col in range(ncols): - # generate the cathode resistor, guaranteed one per column - self.res[str(col)] = res = self.Block(res_model) - connect_passive_io(col, res.b) - for row in range(nrows): - self.led[f"{row}_{col}"] = led = self.Block(led_model) - self.connect(led.k, res.a) - if row >= col: # displaced by resistor - connect_passive_io(row + 1, led.a) - else: - connect_passive_io(row, led.a) - - # generate the adapters and connect the internal passive IO to external typed IO - for index, passive_io in passive_ios.items(): - # if there is a cathode resistor attached to this index, then include the sunk current - if index < ncols: - sink_res = self.res[str(index)] - sink_current = -(io_voltage / sink_res.actual_resistance).upper() * ncols - else: - sink_current = 0 * mAmp - - # then add the maximum of the LED source currents, for the rest of the cathode lines - source_current = 0 * mAmp - for col in range(ncols): - col_res = self.res[str(col)] - source_current = (io_voltage / col_res.actual_resistance).upper().max(source_current) - - self.connect(self.ios.append_elt(DigitalSink.empty(), str(index)), - passive_io.adapt_to(DigitalSink(current_draw=(sink_current, source_current)))) + @override + def _svgpcb_bbox(self) -> Tuple[float, float, float, float]: + return ( + -1.0, + -1.0, + self._svgpcb_get(self.ncols) * 0.2 * 25.4 + 1.0, + (self._svgpcb_get(self.nrows) + 1) * 0.2 * 25.4 + 1.0, + ) + + def __init__( + self, nrows: IntLike, ncols: IntLike, color: LedColorLike = Led.Any, current_draw: RangeLike = (1, 10) * mAmp + ): + super().__init__() + + self.current_draw = self.ArgParameter(current_draw) + self.color = self.ArgParameter(color) + + # note that IOs supply both the positive and negative + self.ios = self.Port(Vector(DigitalSink.empty())) + + self.nrows = self.ArgParameter(nrows) + self.ncols = self.ArgParameter(ncols) + self.generator_param(self.nrows, self.ncols) + + @override + def generate(self) -> None: + super().generate() + nrows = self.get(self.nrows) + ncols = self.get(self.ncols) + + io_voltage = self.ios.hull(lambda x: x.link().voltage) + io_voltage_upper = io_voltage.upper() + io_voltage_lower = self.ios.hull(lambda x: x.link().output_thresholds).upper() + + # internally, this uses passive ports on all the components, and only casts to a DigitalSink at the end + # which is necessary to account for that not all LEDs can be simultaneously on + passive_ios: Dict[int, Passive] = {} # keeps the passive-side port for each boundary IO + + def connect_passive_io(index: int, io: Passive) -> None: + # connects a Passive-typed IO to the index, handling the first and subsequent case + if index in passive_ios: + self.connect(passive_ios[index], io) # subsequent case, actually do the connection + else: + passive_ios[index] = io # first case, just bootstrap the data structure + + self.res = ElementDict[Resistor]() + res_model = Resistor( + resistance=(io_voltage_upper / self.current_draw.upper(), io_voltage_lower / self.current_draw.lower()) + ) + self.led = ElementDict[Led]() + led_model = Led(color=self.color) + + # generate the resistor and LEDs for each column + for col in range(ncols): + # generate the cathode resistor, guaranteed one per column + self.res[str(col)] = res = self.Block(res_model) + connect_passive_io(col, res.b) + for row in range(nrows): + self.led[f"{row}_{col}"] = led = self.Block(led_model) + self.connect(led.k, res.a) + if row >= col: # displaced by resistor + connect_passive_io(row + 1, led.a) + else: + connect_passive_io(row, led.a) + + # generate the adapters and connect the internal passive IO to external typed IO + for index, passive_io in passive_ios.items(): + # if there is a cathode resistor attached to this index, then include the sunk current + if index < ncols: + sink_res = self.res[str(index)] + sink_current = -(io_voltage / sink_res.actual_resistance).upper() * ncols + else: + sink_current = 0 * mAmp + + # then add the maximum of the LED source currents, for the rest of the cathode lines + source_current = 0 * mAmp + for col in range(ncols): + col_res = self.res[str(col)] + source_current = (io_voltage / col_res.actual_resistance).upper().max(source_current) + + self.connect( + self.ios.append_elt(DigitalSink.empty(), str(index)), + passive_io.adapt_to(DigitalSink(current_draw=(sink_current, source_current))), + ) diff --git a/edg/parts/Leds.py b/edg/parts/Leds.py index 4f04f329d..aa61807d6 100644 --- a/edg/parts/Leds.py +++ b/edg/parts/Leds.py @@ -5,80 +5,88 @@ class SmtLed(Led, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'D', 'LED_SMD:LED_0603_1608Metric', - { - '2': self.a, - '1': self.k, - }, - part='LED', - ) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "D", + "LED_SMD:LED_0603_1608Metric", + { + "2": self.a, + "1": self.k, + }, + part="LED", + ) class ThtLed(Led, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'D', 'LED_THT:LED_D5.0mm', - { - '2': self.a, - '1': self.k, - }, - part='LED', - ) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "D", + "LED_THT:LED_D5.0mm", + { + "2": self.a, + "1": self.k, + }, + part="LED", + ) class Smt0606RgbLed(RgbLedCommonAnode, JlcPart, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'D', 'LED_SMD:LED_LiteOn_LTST-C19HE1WT', - { # ABGR configuration - also pins 1/2 and 3/4 are swapped on this pattern - '2': self.a, - '1': self.k_blue, - '4': self.k_green, - '3': self.k_red, - }, - mfr='Everlight Electronics Co Ltd', part='EAST1616RGBB2' - ) - self.assign(self.lcsc_part, 'C264517') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "D", + "LED_SMD:LED_LiteOn_LTST-C19HE1WT", + { # ABGR configuration - also pins 1/2 and 3/4 are swapped on this pattern + "2": self.a, + "1": self.k_blue, + "4": self.k_green, + "3": self.k_red, + }, + mfr="Everlight Electronics Co Ltd", + part="EAST1616RGBB2", + ) + self.assign(self.lcsc_part, "C264517") + self.assign(self.actual_basic_part, False) class Smt0404RgbLed(RgbLedCommonAnode, JlcPart, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'D', 'LED_SMD:LED_Lumex_SML-LX0404SIUPGUSB', - { - '1': self.a, - '2': self.k_red, - '3': self.k_green, - '4': self.k_blue, - }, - mfr='Foshan NationStar Optoelectronics', part='FC-B1010RGBT-HG' - ) - self.assign(self.lcsc_part, 'C158099') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "D", + "LED_SMD:LED_Lumex_SML-LX0404SIUPGUSB", + { + "1": self.a, + "2": self.k_red, + "3": self.k_green, + "4": self.k_blue, + }, + mfr="Foshan NationStar Optoelectronics", + part="FC-B1010RGBT-HG", + ) + self.assign(self.lcsc_part, "C158099") + self.assign(self.actual_basic_part, False) class ThtRgbLed(RgbLedCommonAnode, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'D', 'LED_THT:LED_D5.0mm-4_RGB_Staggered_Pins', - { # RAGB configuration - '1': self.k_red, - '2': self.a, - '3': self.k_green, - '4': self.k_blue, - }, - mfr='Sparkfun', part='COM-09264' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "D", + "LED_THT:LED_D5.0mm-4_RGB_Staggered_Pins", + { # RAGB configuration + "1": self.k_red, + "2": self.a, + "3": self.k_green, + "4": self.k_blue, + }, + mfr="Sparkfun", + part="COM-09264", + ) diff --git a/edg/parts/LightSensor_As7341.py b/edg/parts/LightSensor_As7341.py index 98082d309..04555d9ad 100644 --- a/edg/parts/LightSensor_As7341.py +++ b/edg/parts/LightSensor_As7341.py @@ -7,44 +7,47 @@ class As7341_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(1.7, 2.0)*Volt, - current_draw=(0.7, 300)*uAmp # typ sleep to max active - )) + self.vdd = self.Port( + VoltageSink(voltage_limits=(1.7, 2.0) * Volt, current_draw=(0.7, 300) * uAmp) # typ sleep to max active + ) self.gnd = self.Port(Ground()) dio_model = DigitalBidir.from_supply( - self.gnd, self.vdd, - voltage_limit_abs=(-0.3, 3.6)*Volt, - input_threshold_abs=(0.54, 1.26)*Volt, - output_threshold_abs=(0, float('inf'))*Volt # reflects pulldown + self.gnd, + self.vdd, + voltage_limit_abs=(-0.3, 3.6) * Volt, + input_threshold_abs=(0.54, 1.26) * Volt, + output_threshold_abs=(0, float("inf")) * Volt, # reflects pulldown ) self.i2c = self.Port(I2cTarget(dio_model, [0x39])) @override def contents(self) -> None: self.footprint( - 'U', 'Package_LGA:AMS_OLGA-8_2x3.1mm_P0.8mm', + "U", + "Package_LGA:AMS_OLGA-8_2x3.1mm_P0.8mm", { - '1': self.vdd, - '2': self.i2c.scl, - '3': self.gnd, + "1": self.vdd, + "2": self.i2c.scl, + "3": self.gnd, # '4': self.ldr, - '5': self.gnd, # PGND + "5": self.gnd, # PGND # '6': self.gpio, # '7': self.int, - '8': self.i2c.sda, + "8": self.i2c.sda, }, - mfr='ams', part='AS7341-DLGT', - datasheet='https://mm.digikey.com/Volume0/opasdata/d220001/medias/docus/324/AS7341_DS.pdf' + mfr="ams", + part="AS7341-DLGT", + datasheet="https://mm.digikey.com/Volume0/opasdata/d220001/medias/docus/324/AS7341_DS.pdf", ) - self.assign(self.lcsc_part, 'C2655145') + self.assign(self.lcsc_part, "C2655145") self.assign(self.actual_basic_part, False) class As7341(LightSensor, Block): """11-channel spectral sensor, from 350nm to 1000nm, with 8 visible light channels, a NIR channel, a non-filtered ("clear" wideband) channel, and a flicker detection channel""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(As7341_Device()) @@ -56,4 +59,4 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() # capacitance value assumed, same value as on Adafruit's breakout - self.vdd_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + self.vdd_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) diff --git a/edg/parts/LightSensor_Bh1750.py b/edg/parts/LightSensor_Bh1750.py index 8b2964186..aa1858c15 100644 --- a/edg/parts/LightSensor_Bh1750.py +++ b/edg/parts/LightSensor_Bh1750.py @@ -7,40 +7,42 @@ class Bh1750_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() - self.vcc = self.Port(VoltageSink( - voltage_limits=(2.4, 3.6)*Volt, - current_draw=(0.01*uAmp, 190*uAmp) # typ powerdown to max supply current - )) + self.vcc = self.Port( + VoltageSink( + voltage_limits=(2.4, 3.6) * Volt, + current_draw=(0.01 * uAmp, 190 * uAmp), # typ powerdown to max supply current + ) + ) self.dvi = self.Port(Passive()) # some kind of reset pin self.gnd = self.Port(Ground()) - dio_model = DigitalBidir.from_supply( - self.gnd, self.vcc, - input_threshold_factor=(0.3, 0.7) - ) + dio_model = DigitalBidir.from_supply(self.gnd, self.vcc, input_threshold_factor=(0.3, 0.7)) self.i2c = self.Port(I2cTarget(dio_model, [0x23])) @override def contents(self) -> None: self.footprint( - 'U', 'Package_TO_SOT_SMD:HVSOF6', + "U", + "Package_TO_SOT_SMD:HVSOF6", { - '1': self.vcc, - '2': self.gnd, # TODO address support - '3': self.gnd, - '4': self.i2c.sda, - '5': self.dvi, - '6': self.i2c.scl, + "1": self.vcc, + "2": self.gnd, # TODO address support + "3": self.gnd, + "4": self.i2c.sda, + "5": self.dvi, + "6": self.i2c.scl, }, - mfr='Rohm Semiconductor', part='BH1750', - datasheet='https://www.mouser.com/datasheet/2/348/bh1750fvi-e-186247.pdf' + mfr="Rohm Semiconductor", + part="BH1750", + datasheet="https://www.mouser.com/datasheet/2/348/bh1750fvi-e-186247.pdf", ) - self.assign(self.lcsc_part, 'C78960') + self.assign(self.lcsc_part, "C78960") self.assign(self.actual_basic_part, False) class Bh1750(LightSensor, Block): """16-bit ambient light sensor, 1-65535 lx""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Bh1750_Device()) @@ -51,10 +53,10 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() # capacitors from shuttle board example - self.vcc_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vcc) - self.dvi_res = self.Block(Resistor(1*kOhm(tol=0.05))) + self.vcc_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vcc) + self.dvi_res = self.Block(Resistor(1 * kOhm(tol=0.05))) self.connect(self.dvi_res.a.adapt_to(VoltageSink()), self.pwr) self.connect(self.dvi_res.b, self.ic.dvi) - self.dvi_cap = self.Block(Capacitor(1*uFarad(tol=0.2), self.pwr.link().voltage)) + self.dvi_cap = self.Block(Capacitor(1 * uFarad(tol=0.2), self.pwr.link().voltage)) self.connect(self.dvi_cap.pos, self.ic.dvi) self.connect(self.dvi_cap.neg.adapt_to(Ground()), self.gnd) diff --git a/edg/parts/LinearRegulators.py b/edg/parts/LinearRegulators.py index 0d159f8b3..c8860a8c6 100644 --- a/edg/parts/LinearRegulators.py +++ b/edg/parts/LinearRegulators.py @@ -7,704 +7,792 @@ class Ld1117_Device(InternalSubcircuit, LinearRegulatorDevice, GeneratorBlock, JlcPart, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.assign(self.pwr_in.voltage_limits, (0, 15) * Volt) - self.assign(self.pwr_out.current_limits, (0, 0.8) * Amp) # most conservative estimate, up to 1300mA - self.assign(self.actual_quiescent_current, (5, 10) * mAmp) - self.assign(self.actual_dropout, (0, 1.2) * Volt) - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage) - - @override - def generate(self) -> None: - super().generate() - parts = [ # output voltage - (Range(1.140, 1.260), 'LD1117S12TR', 'C155612'), - (Range(1.76, 1.84), 'LD1117S18TR', 'C80598'), - (Range(2.45, 2.55), 'LD1117S25TR', 'C26457'), - (Range(3.235, 3.365), 'LD1117S33TR', 'C86781'), - (Range(4.9, 5.1), 'LD1117S50TR', 'C134077'), - ] - suitable_parts = [(part_out, part_number, lcsc_part) for part_out, part_number, lcsc_part in parts - if part_out.fuzzy_in(self.get(self.output_voltage))] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage, part_number, lcsc_part = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage) - self.assign(self.lcsc_part, lcsc_part) - self.assign(self.actual_basic_part, False) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-223-3_TabPin2', - { - '1': self.gnd, - '2': self.pwr_out, - '3': self.pwr_in, - }, - mfr='STMicroelectronics', part=part_number, - datasheet='https://www.st.com/content/ccc/resource/technical/document/datasheet/99/3b/7d/91/91/51/4b/be/CD00000544.pdf/files/CD00000544.pdf/jcr:content/translations/en.CD00000544.pdf', - ) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.assign(self.pwr_in.voltage_limits, (0, 15) * Volt) + self.assign(self.pwr_out.current_limits, (0, 0.8) * Amp) # most conservative estimate, up to 1300mA + self.assign(self.actual_quiescent_current, (5, 10) * mAmp) + self.assign(self.actual_dropout, (0, 1.2) * Volt) + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage) + + @override + def generate(self) -> None: + super().generate() + parts = [ # output voltage + (Range(1.140, 1.260), "LD1117S12TR", "C155612"), + (Range(1.76, 1.84), "LD1117S18TR", "C80598"), + (Range(2.45, 2.55), "LD1117S25TR", "C26457"), + (Range(3.235, 3.365), "LD1117S33TR", "C86781"), + (Range(4.9, 5.1), "LD1117S50TR", "C134077"), + ] + suitable_parts = [ + (part_out, part_number, lcsc_part) + for part_out, part_number, lcsc_part in parts + if part_out.fuzzy_in(self.get(self.output_voltage)) + ] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage, part_number, lcsc_part = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage) + self.assign(self.lcsc_part, lcsc_part) + self.assign(self.actual_basic_part, False) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + { + "1": self.gnd, + "2": self.pwr_out, + "3": self.pwr_in, + }, + mfr="STMicroelectronics", + part=part_number, + datasheet="https://www.st.com/content/ccc/resource/technical/document/datasheet/99/3b/7d/91/91/51/4b/be/CD00000544.pdf/files/CD00000544.pdf/jcr:content/translations/en.CD00000544.pdf", + ) class Ld1117(LinearRegulator): - @override - def contents(self) -> None: - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Ld1117_Device(self.output_voltage)) - self.in_cap = imp.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))) - self.out_cap = imp.Block(DecouplingCapacitor(capacitance=10 * uFarad(tol=0.2))) + @override + def contents(self) -> None: + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Ld1117_Device(self.output_voltage)) + self.in_cap = imp.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))) + self.out_cap = imp.Block(DecouplingCapacitor(capacitance=10 * uFarad(tol=0.2))) - self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) - self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) + self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) + self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) class Ldl1117_Device(InternalSubcircuit, LinearRegulatorDevice, GeneratorBlock, JlcPart, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.assign(self.pwr_in.voltage_limits, (2.6, 18) * Volt) - self.assign(self.pwr_out.current_limits, (0, 1.5) * Amp) # most conservative estimate, typ up to 2A - self.assign(self.actual_quiescent_current, (0, 500) * uAmp) # typ is 250uA - self.assign(self.actual_dropout, (0, 0.6) * Volt) # worst-case, typ is 0.35 - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage) - - @override - def generate(self) -> None: - super().generate() - TOLERANCE = 0.03 # worst-case -40 < Tj < 125C, slightly better at 25C - parts = [ # output voltage - (1.185, 'LDL1117S12R', 'C2926949'), - # (1.5, 'LDL1117S15R'), # not listed at JLC - (1.8, 'LDL1117S18R', 'C2926947'), - (2.5, 'LDL1117S25R', 'C2926456'), - (3.0, 'LDL1117S30R', 'C2798214'), - (3.3, 'LDL1117S33R', 'C435835'), - (5.0, 'LDL1117S50R', 'C970558'), - ] - suitable_parts = [part for part in parts - if Range.from_tolerance(part[0], TOLERANCE) in self.get(self.output_voltage)] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage_nominal, part_number, jlc_number = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage_nominal * Volt(tol=TOLERANCE)) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-223-3_TabPin2', - { - '1': self.gnd, - '2': self.pwr_out, - '3': self.pwr_in, - }, - mfr='STMicroelectronics', part=part_number, - datasheet='https://www.st.com/content/ccc/resource/technical/document/datasheet/group3/0e/5a/00/ca/10/1a/4f/a5/DM00366442/files/DM00366442.pdf/jcr:content/translations/en.DM00366442.pdf', - ) - self.assign(self.lcsc_part, jlc_number) - self.assign(self.actual_basic_part, False) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.assign(self.pwr_in.voltage_limits, (2.6, 18) * Volt) + self.assign(self.pwr_out.current_limits, (0, 1.5) * Amp) # most conservative estimate, typ up to 2A + self.assign(self.actual_quiescent_current, (0, 500) * uAmp) # typ is 250uA + self.assign(self.actual_dropout, (0, 0.6) * Volt) # worst-case, typ is 0.35 + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage) + + @override + def generate(self) -> None: + super().generate() + TOLERANCE = 0.03 # worst-case -40 < Tj < 125C, slightly better at 25C + parts = [ # output voltage + (1.185, "LDL1117S12R", "C2926949"), + # (1.5, 'LDL1117S15R'), # not listed at JLC + (1.8, "LDL1117S18R", "C2926947"), + (2.5, "LDL1117S25R", "C2926456"), + (3.0, "LDL1117S30R", "C2798214"), + (3.3, "LDL1117S33R", "C435835"), + (5.0, "LDL1117S50R", "C970558"), + ] + suitable_parts = [ + part for part in parts if Range.from_tolerance(part[0], TOLERANCE) in self.get(self.output_voltage) + ] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage_nominal, part_number, jlc_number = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage_nominal * Volt(tol=TOLERANCE)) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + { + "1": self.gnd, + "2": self.pwr_out, + "3": self.pwr_in, + }, + mfr="STMicroelectronics", + part=part_number, + datasheet="https://www.st.com/content/ccc/resource/technical/document/datasheet/group3/0e/5a/00/ca/10/1a/4f/a5/DM00366442/files/DM00366442.pdf/jcr:content/translations/en.DM00366442.pdf", + ) + self.assign(self.lcsc_part, jlc_number) + self.assign(self.actual_basic_part, False) class Ldl1117(LinearRegulator): - """A series of fixed-output, general-purpose, low-dropout linear regulators in SOT-223 and - supporting up to 18V input and 1.2A draw. - """ - @override - def contents(self) -> None: - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Ldl1117_Device(self.output_voltage)) - self.in_cap = imp.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))) - self.out_cap = imp.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))) - - self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) - self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) + """A series of fixed-output, general-purpose, low-dropout linear regulators in SOT-223 and + supporting up to 18V input and 1.2A draw. + """ + + @override + def contents(self) -> None: + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Ldl1117_Device(self.output_voltage)) + self.in_cap = imp.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))) + self.out_cap = imp.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))) + + self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) + self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) class Ap2204k_Device(InternalSubcircuit, LinearRegulatorDevice, GeneratorBlock, JlcPart, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - # Part datasheet, Recommended Operating Conditions - self.assign(self.pwr_in.voltage_limits, (0, 24) * Volt) - self.assign(self.pwr_out.current_limits, (0, 0.15) * Amp) - self.assign(self.actual_quiescent_current, (0.020, 2.5) * mAmp) # ground current, not including standby current - self.assign(self.actual_dropout, (0, 0.5) * Volt) # worst-case, 150mA Iout - - self.en = self.Port(DigitalSink( - voltage_limits=(0, 24) * Volt, - current_draw=(0, 1)*uAmp, # TYP rating, min/max bounds not given - input_thresholds=(0.4, 2.0)*Volt - )) - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage) - - @override - def generate(self) -> None: - super().generate() - - TOLERANCE = 0.02 - parts = [ - # output voltage, quiescent current - (5, 'AP2204K-5.0', 'C112031'), - (3.3, 'AP2204K-3.3', 'C112032'), - (3.0, 'AP2204K-3.0', 'C460339'), - # (2.8, 'AP2204K-2.8'), # not stocked at JLC - # (2.5, 'AP2204K-2.5'), - (1.8, 'AP2204K-1.8', 'C460338'), - # (1.5, 'AP2204K-1.5'), - ] - suitable_parts = [part for part in parts - if Range.from_tolerance(part[0], TOLERANCE) in self.get(self.output_voltage)] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage_nominal, part_number, jlc_number = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage_nominal * Volt(tol=TOLERANCE)) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.pwr_in, - '2': self.gnd, - '3': self.en, - # pin 4 is ADJ/NC - '5': self.pwr_out, - }, - mfr='Diodes Incorporated', part=part_number, - datasheet='https://www.diodes.com/assets/Datasheets/AP2204.pdf' - ) - self.assign(self.lcsc_part, jlc_number) - self.assign(self.actual_basic_part, False) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + # Part datasheet, Recommended Operating Conditions + self.assign(self.pwr_in.voltage_limits, (0, 24) * Volt) + self.assign(self.pwr_out.current_limits, (0, 0.15) * Amp) + self.assign(self.actual_quiescent_current, (0.020, 2.5) * mAmp) # ground current, not including standby current + self.assign(self.actual_dropout, (0, 0.5) * Volt) # worst-case, 150mA Iout + + self.en = self.Port( + DigitalSink( + voltage_limits=(0, 24) * Volt, + current_draw=(0, 1) * uAmp, # TYP rating, min/max bounds not given + input_thresholds=(0.4, 2.0) * Volt, + ) + ) + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage) + + @override + def generate(self) -> None: + super().generate() + + TOLERANCE = 0.02 + parts = [ + # output voltage, quiescent current + (5, "AP2204K-5.0", "C112031"), + (3.3, "AP2204K-3.3", "C112032"), + (3.0, "AP2204K-3.0", "C460339"), + # (2.8, 'AP2204K-2.8'), # not stocked at JLC + # (2.5, 'AP2204K-2.5'), + (1.8, "AP2204K-1.8", "C460338"), + # (1.5, 'AP2204K-1.5'), + ] + suitable_parts = [ + part for part in parts if Range.from_tolerance(part[0], TOLERANCE) in self.get(self.output_voltage) + ] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage_nominal, part_number, jlc_number = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage_nominal * Volt(tol=TOLERANCE)) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.pwr_in, + "2": self.gnd, + "3": self.en, + # pin 4 is ADJ/NC + "5": self.pwr_out, + }, + mfr="Diodes Incorporated", + part=part_number, + datasheet="https://www.diodes.com/assets/Datasheets/AP2204.pdf", + ) + self.assign(self.lcsc_part, jlc_number) + self.assign(self.actual_basic_part, False) class Ap2204k(VoltageRegulatorEnableWrapper, LinearRegulator): - """AP2204K block providing the LinearRegulator interface and optional enable (tied high if not connected). - """ - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.en - - @override - def contents(self) -> None: - super().contents() - self.ic = self.Block(Ap2204k_Device(self.output_voltage)) - self.connect(self.pwr_in, self.ic.pwr_in) - self.connect(self.pwr_out, self.ic.pwr_out) - self.connect(self.gnd, self.ic.gnd) - self.in_cap = self.Block(DecouplingCapacitor(capacitance=1.1 * uFarad(tol=0.2)))\ - .connected(self.gnd, self.pwr_in) - self.out_cap = self.Block(DecouplingCapacitor(capacitance=2.2 * uFarad(tol=0.2)))\ - .connected(self.gnd, self.pwr_out) + """AP2204K block providing the LinearRegulator interface and optional enable (tied high if not connected).""" + + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.en + + @override + def contents(self) -> None: + super().contents() + self.ic = self.Block(Ap2204k_Device(self.output_voltage)) + self.connect(self.pwr_in, self.ic.pwr_in) + self.connect(self.pwr_out, self.ic.pwr_out) + self.connect(self.gnd, self.ic.gnd) + self.in_cap = self.Block(DecouplingCapacitor(capacitance=1.1 * uFarad(tol=0.2))).connected( + self.gnd, self.pwr_in + ) + self.out_cap = self.Block(DecouplingCapacitor(capacitance=2.2 * uFarad(tol=0.2))).connected( + self.gnd, self.pwr_out + ) class Ap7215_Device(InternalSubcircuit, LinearRegulatorDevice, JlcPart, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - # Part datasheet, Recommended Operating Conditions - self.assign(self.pwr_in.voltage_limits, (3.3, 5.5) * Volt) - self.assign(self.pwr_out.current_limits, (0, 0.6) * Amp) - self.assign(self.actual_quiescent_current, (50, 80) * uAmp) - self.assign(self.actual_dropout, (0, 0.25) * Volt) # worst-case @ 100mA Iout - self.assign(self.pwr_out.voltage_out, (3.234, 3.366) * Volt) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-89-3', - { - '1': self.gnd, - '2': self.pwr_in, - '3': self.pwr_out, - }, - mfr='Diodes Incorporated', part='AP7215-33YG-13', - datasheet='https://www.diodes.com/assets/Datasheets/AP7215.pdf' - ) - self.assign(self.lcsc_part, 'C460367') - self.assign(self.actual_basic_part, False) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + # Part datasheet, Recommended Operating Conditions + self.assign(self.pwr_in.voltage_limits, (3.3, 5.5) * Volt) + self.assign(self.pwr_out.current_limits, (0, 0.6) * Amp) + self.assign(self.actual_quiescent_current, (50, 80) * uAmp) + self.assign(self.actual_dropout, (0, 0.25) * Volt) # worst-case @ 100mA Iout + self.assign(self.pwr_out.voltage_out, (3.234, 3.366) * Volt) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-89-3", + { + "1": self.gnd, + "2": self.pwr_in, + "3": self.pwr_out, + }, + mfr="Diodes Incorporated", + part="AP7215-33YG-13", + datasheet="https://www.diodes.com/assets/Datasheets/AP7215.pdf", + ) + self.assign(self.lcsc_part, "C460367") + self.assign(self.actual_basic_part, False) class Ap7215(LinearRegulator): - """AP7215 fixed 3.3v LDO in SOT-89 providing the LinearRegulator interface. - """ - @override - def contents(self) -> None: - super().contents() - self.ic = self.Block(Ap7215_Device(self.output_voltage)) - self.connect(self.pwr_in, self.ic.pwr_in) - self.connect(self.pwr_out, self.ic.pwr_out) - self.connect(self.gnd, self.ic.gnd) - self.in_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.pwr_in) - self.out_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.pwr_out) + """AP7215 fixed 3.3v LDO in SOT-89 providing the LinearRegulator interface.""" + + @override + def contents(self) -> None: + super().contents() + self.ic = self.Block(Ap7215_Device(self.output_voltage)) + self.connect(self.pwr_in, self.ic.pwr_in) + self.connect(self.pwr_out, self.ic.pwr_out) + self.connect(self.gnd, self.ic.gnd) + self.in_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr_in) + self.out_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.pwr_out + ) class Xc6206p_Device(InternalSubcircuit, LinearRegulatorDevice, GeneratorBlock, JlcPart, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.assign(self.pwr_in.voltage_limits, (1.8, 6.0) * Volt) - self.assign(self.actual_quiescent_current, (1, 3) * uAmp) # supply current - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage) - - @override - def generate(self) -> None: - super().generate() - parts = [ # output range, part number, (dropout typ @10mA, max @100mA), max current, lcsc, basic part - # +/-0.03v tolerance for Vout < 1.5v - (Range.from_abs_tolerance(1.2, 0.03), 'XC6206P122MR-G', (0.46, 0.96), 0.06, 'C424699', False), - (Range.from_abs_tolerance(1.2, 0.03), 'XC6206P132MR', (0.46, 0.96), 0.06, 'C424700', False), - (Range.from_tolerance(1.5, 0.02), 'XC6206P152MR', (0.30, 0.86), 0.06, 'C424701', False), - (Range.from_tolerance(1.8, 0.02), 'XC6206P182MR-G', (0.15, 0.78), 0.08, 'C2831490', False), - (Range.from_tolerance(1.8, 0.02), 'XC6206P182MR', (0.15, 0.78), 0.08, 'C21659', False), # non -G version, higher stock - - (Range.from_tolerance(2.0, 0.02), 'XC6206P202MR-G', (0.10, 0.78), 0.12, 'C2891260', False), - (Range.from_tolerance(2.5, 0.02), 'XC6206P252MR', (0.10, 0.71), 0.15, 'C15906', False), - (Range.from_tolerance(2.8, 0.02), 'XC6206P282MR', (0.10, 0.71), 0.15, 'C14255', False), - - (Range.from_tolerance(3.0, 0.02), 'XC6206P302MR-G', (0.075, 0.68), 0.20, 'C9972', False), - (Range.from_tolerance(3.3, 0.02), 'XC6206P332MR', (0.075, 0.68), 0.20, 'C5446', True), # basic part preferred - (Range.from_tolerance(3.3, 0.02), 'XC6206P332MR-G', (0.075, 0.68), 0.20, 'C2891264', False), - (Range.from_tolerance(3.6, 0.02), 'XC6206P362MR-G', (0.075, 0.68), 0.20, 'C34705', False), - - (Range.from_tolerance(4.0, 0.02), 'XC6206P402MR', (0.06, 0.63), 0.25, 'C484266', False), - - (Range.from_tolerance(5.0, 0.02), 'XC6206P502MR', (0.05, 0.60), 0.25, 'C16767', False), - ] - suitable_parts = [part for part in parts - if part[0] in self.get(self.output_voltage)] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage, part_number, part_dropout, part_max_current, lcsc_part, basic_part = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage * Volt) - self.assign(self.actual_dropout, part_dropout * Volt) - self.assign(self.pwr_out.current_limits, (0, part_max_current) * Amp) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', - { - '1': self.gnd, - '2': self.pwr_out, - '3': self.pwr_in, - }, - mfr='Torex Semiconductor Ltd', part=part_number, - datasheet='https://product.torexsemi.com/system/files/series/xc6206.pdf', - ) - self.assign(self.lcsc_part, lcsc_part) - self.assign(self.actual_basic_part, basic_part) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.assign(self.pwr_in.voltage_limits, (1.8, 6.0) * Volt) + self.assign(self.actual_quiescent_current, (1, 3) * uAmp) # supply current + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage) + + @override + def generate(self) -> None: + super().generate() + parts = [ # output range, part number, (dropout typ @10mA, max @100mA), max current, lcsc, basic part + # +/-0.03v tolerance for Vout < 1.5v + (Range.from_abs_tolerance(1.2, 0.03), "XC6206P122MR-G", (0.46, 0.96), 0.06, "C424699", False), + (Range.from_abs_tolerance(1.2, 0.03), "XC6206P132MR", (0.46, 0.96), 0.06, "C424700", False), + (Range.from_tolerance(1.5, 0.02), "XC6206P152MR", (0.30, 0.86), 0.06, "C424701", False), + (Range.from_tolerance(1.8, 0.02), "XC6206P182MR-G", (0.15, 0.78), 0.08, "C2831490", False), + ( + Range.from_tolerance(1.8, 0.02), + "XC6206P182MR", + (0.15, 0.78), + 0.08, + "C21659", + False, + ), # non -G version, higher stock + (Range.from_tolerance(2.0, 0.02), "XC6206P202MR-G", (0.10, 0.78), 0.12, "C2891260", False), + (Range.from_tolerance(2.5, 0.02), "XC6206P252MR", (0.10, 0.71), 0.15, "C15906", False), + (Range.from_tolerance(2.8, 0.02), "XC6206P282MR", (0.10, 0.71), 0.15, "C14255", False), + (Range.from_tolerance(3.0, 0.02), "XC6206P302MR-G", (0.075, 0.68), 0.20, "C9972", False), + ( + Range.from_tolerance(3.3, 0.02), + "XC6206P332MR", + (0.075, 0.68), + 0.20, + "C5446", + True, + ), # basic part preferred + (Range.from_tolerance(3.3, 0.02), "XC6206P332MR-G", (0.075, 0.68), 0.20, "C2891264", False), + (Range.from_tolerance(3.6, 0.02), "XC6206P362MR-G", (0.075, 0.68), 0.20, "C34705", False), + (Range.from_tolerance(4.0, 0.02), "XC6206P402MR", (0.06, 0.63), 0.25, "C484266", False), + (Range.from_tolerance(5.0, 0.02), "XC6206P502MR", (0.05, 0.60), 0.25, "C16767", False), + ] + suitable_parts = [part for part in parts if part[0] in self.get(self.output_voltage)] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage, part_number, part_dropout, part_max_current, lcsc_part, basic_part = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage * Volt) + self.assign(self.actual_dropout, part_dropout * Volt) + self.assign(self.pwr_out.current_limits, (0, part_max_current) * Amp) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23", + { + "1": self.gnd, + "2": self.pwr_out, + "3": self.pwr_in, + }, + mfr="Torex Semiconductor Ltd", + part=part_number, + datasheet="https://product.torexsemi.com/system/files/series/xc6206.pdf", + ) + self.assign(self.lcsc_part, lcsc_part) + self.assign(self.actual_basic_part, basic_part) class Xc6206p(LinearRegulator): - """XC6206P LDOs in SOT-23 which seem popular in some open-source designs and some are JLC basic parts.""" - @override - def contents(self) -> None: - with self.implicit_connect( + """XC6206P LDOs in SOT-23 which seem popular in some open-source designs and some are JLC basic parts.""" + + @override + def contents(self) -> None: + with self.implicit_connect( ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Xc6206p_Device(output_voltage=self.output_voltage)) - self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) - self.out_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) + ) as imp: + self.ic = imp.Block(Xc6206p_Device(output_voltage=self.output_voltage)) + self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) + self.out_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) - self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) - self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) + self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) + self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) class Xc6209_Device(InternalSubcircuit, LinearRegulatorDevice, GeneratorBlock, JlcPart, FootprintBlock): - # Also pin-compatible with MCP1802 and NJM2882F (which has a noise bypass pin) - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.assign(self.pwr_in.voltage_limits, (2, 10) * Volt) - self.assign(self.pwr_out.current_limits, (0, 300) * mAmp) - self.assign(self.actual_quiescent_current, (0.01, 50) * uAmp) # typ is 250uA - - self.ce = self.Port(DigitalSink.from_supply( - self.gnd, self.pwr_in, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - current_draw=(0.1, 5.0)*uAmp, - input_threshold_abs=(0.25, 1.6)*Volt - )) - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage) - - @override - def generate(self) -> None: - super().generate() - TOLERANCE = 0.02 # worst-case -40 < Tj < 125C, slightly better at 25C - parts = [ # output voltage, part number, (dropout typ @ 30mA, dropout max @ 100mA), max current, lcsc - (1.5, 'XC6209F152MR-G', (0.50, 0.60), 'C216623'), - (3.3, 'XC6209F332MR-G', (0.06, 0.25), 'C216624'), - (5.0, 'XC6209F502MR-G', (0.05, 0.21), 'C222571'), - ] - suitable_parts = [part for part in parts - if Range.from_tolerance(part[0], TOLERANCE) in self.get(self.output_voltage)] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage_nominal, part_number, part_dropout, lcsc_part = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage_nominal * Volt(tol=TOLERANCE)) - self.assign(self.actual_dropout, part_dropout * Volt) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.pwr_in, - '2': self.gnd, - '3': self.ce, - # pin 4 is NC - '5': self.pwr_out, - }, - mfr='Torex Semiconductor Ltd', part=part_number, - datasheet='https://www.torexsemi.com/file/en/products/discontinued/-2016/53-XC6209_12.pdf', - ) - self.assign(self.lcsc_part, lcsc_part) - self.assign(self.actual_basic_part, False) + # Also pin-compatible with MCP1802 and NJM2882F (which has a noise bypass pin) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.assign(self.pwr_in.voltage_limits, (2, 10) * Volt) + self.assign(self.pwr_out.current_limits, (0, 300) * mAmp) + self.assign(self.actual_quiescent_current, (0.01, 50) * uAmp) # typ is 250uA + + self.ce = self.Port( + DigitalSink.from_supply( + self.gnd, + self.pwr_in, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_draw=(0.1, 5.0) * uAmp, + input_threshold_abs=(0.25, 1.6) * Volt, + ) + ) + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage) + + @override + def generate(self) -> None: + super().generate() + TOLERANCE = 0.02 # worst-case -40 < Tj < 125C, slightly better at 25C + parts = [ # output voltage, part number, (dropout typ @ 30mA, dropout max @ 100mA), max current, lcsc + (1.5, "XC6209F152MR-G", (0.50, 0.60), "C216623"), + (3.3, "XC6209F332MR-G", (0.06, 0.25), "C216624"), + (5.0, "XC6209F502MR-G", (0.05, 0.21), "C222571"), + ] + suitable_parts = [ + part for part in parts if Range.from_tolerance(part[0], TOLERANCE) in self.get(self.output_voltage) + ] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage_nominal, part_number, part_dropout, lcsc_part = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage_nominal * Volt(tol=TOLERANCE)) + self.assign(self.actual_dropout, part_dropout * Volt) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.pwr_in, + "2": self.gnd, + "3": self.ce, + # pin 4 is NC + "5": self.pwr_out, + }, + mfr="Torex Semiconductor Ltd", + part=part_number, + datasheet="https://www.torexsemi.com/file/en/products/discontinued/-2016/53-XC6209_12.pdf", + ) + self.assign(self.lcsc_part, lcsc_part) + self.assign(self.actual_basic_part, False) class Xc6209(VoltageRegulatorEnableWrapper, LinearRegulator): - """XC6209F (F: 300mA version, no pull-down resistor; 2: +/-2% accuracy) - Low-ESR ceramic cap compatible""" - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.ce + """XC6209F (F: 300mA version, no pull-down resistor; 2: +/-2% accuracy) + Low-ESR ceramic cap compatible""" - @override - def contents(self) -> None: - super().contents() + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.ce - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Xc6209_Device(output_voltage=self.output_voltage)) - self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) - self.out_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Xc6209_Device(output_voltage=self.output_voltage)) + self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) + self.out_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) - self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) - self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) + self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) + self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) class Ap2210_Device(InternalSubcircuit, LinearRegulatorDevice, GeneratorBlock, JlcPart, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.assign(self.pwr_in.voltage_limits, (2.5, 13.2) * Volt) - self.assign(self.pwr_out.current_limits, (0, 300) * mAmp) - self.assign(self.actual_quiescent_current, (0.01, 15000) * uAmp) # GND pin current - self.assign(self.actual_dropout, (15, 500) * mVolt) - - self.en = self.Port(DigitalSink( - voltage_limits=(0, 15) * Volt, - current_draw=(0.01, 25)*uAmp, - input_thresholds=(0.4, 2.0)*Volt - )) - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage) - - @override - def generate(self) -> None: - super().generate() - TOLERANCE = 0.02 # worst-case -40 < Tj < 125C, slightly better at 25C - parts = [ # output voltage - (2.5, 'AP2210K-2.5', 'C460340'), - (3.0, 'AP2210K-3.0', None), # JLC part not available - (3.3, 'AP2210K-3.3', 'C176959'), - (5.0, 'AP2210K-5.0', 'C500758'), - ] - suitable_parts = [part for part in parts - if Range.from_tolerance(part[0], TOLERANCE) in self.get(self.output_voltage)] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage_nominal, part_number, jlc_number = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage_nominal * Volt(tol=TOLERANCE)) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.pwr_in, - '2': self.gnd, - '3': self.en, - # pin 4 is BYP, optional - '5': self.pwr_out, - }, - mfr='Diodes Incorporated', part=part_number, - datasheet='https://www.diodes.com/assets/Datasheets/AP2210.pdf', - ) - if jlc_number is not None: - self.assign(self.lcsc_part, jlc_number) - self.assign(self.actual_basic_part, False) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.assign(self.pwr_in.voltage_limits, (2.5, 13.2) * Volt) + self.assign(self.pwr_out.current_limits, (0, 300) * mAmp) + self.assign(self.actual_quiescent_current, (0.01, 15000) * uAmp) # GND pin current + self.assign(self.actual_dropout, (15, 500) * mVolt) + + self.en = self.Port( + DigitalSink( + voltage_limits=(0, 15) * Volt, current_draw=(0.01, 25) * uAmp, input_thresholds=(0.4, 2.0) * Volt + ) + ) + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage) + + @override + def generate(self) -> None: + super().generate() + TOLERANCE = 0.02 # worst-case -40 < Tj < 125C, slightly better at 25C + parts = [ # output voltage + (2.5, "AP2210K-2.5", "C460340"), + (3.0, "AP2210K-3.0", None), # JLC part not available + (3.3, "AP2210K-3.3", "C176959"), + (5.0, "AP2210K-5.0", "C500758"), + ] + suitable_parts = [ + part for part in parts if Range.from_tolerance(part[0], TOLERANCE) in self.get(self.output_voltage) + ] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage_nominal, part_number, jlc_number = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage_nominal * Volt(tol=TOLERANCE)) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.pwr_in, + "2": self.gnd, + "3": self.en, + # pin 4 is BYP, optional + "5": self.pwr_out, + }, + mfr="Diodes Incorporated", + part=part_number, + datasheet="https://www.diodes.com/assets/Datasheets/AP2210.pdf", + ) + if jlc_number is not None: + self.assign(self.lcsc_part, jlc_number) + self.assign(self.actual_basic_part, False) class Ap2210(VoltageRegulatorEnableWrapper, LinearRegulator): - """AP2210 RF ULDO in SOT-23-5 with high PSRR and high(er) voltage tolerant. - """ - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.en - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Ap2210_Device(output_voltage=self.output_voltage)) - self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) - self.out_cap = imp.Block(DecouplingCapacitor(capacitance=2.2*uFarad(tol=0.2))) - - self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) - self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) - - -class Lp5907_Device(InternalSubcircuit, LinearRegulatorDevice, SelectorFootprint, PartsTablePart, JlcPart, - GeneratorBlock, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.assign(self.pwr_in.voltage_limits, (2.2, 5.5) * Volt) - self.assign(self.pwr_out.current_limits, (0, 250) * mAmp) - self.assign(self.actual_quiescent_current, (0.2, 425) * uAmp) - self.assign(self.actual_dropout, (50, 250) * mVolt) - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage, self.part, self.footprint_spec) - - self.en = self.Port(DigitalSink( - voltage_limits=(0, 5.5) * Volt, - current_draw=(0.001, 5.5)*uAmp, - input_thresholds=(0.4, 1.2)*Volt - )) - - @override - def generate(self) -> None: - super().generate() - parts = [ # output voltage, Table in 6.5 tolerance varies by output voltage - (Range.from_tolerance(1.2, 0.03), 'LP5907MFX-1.2/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C73478'), - (Range.from_tolerance(1.5, 0.03), 'LP5907MFX-1.5/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C133570'), - (Range.from_tolerance(1.8, 0.02), 'LP5907MFX-1.8/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C92498'), - (Range.from_tolerance(2.5, 0.02), 'LP5907MFX-2.5/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C165132'), - # (Range.from_tolerance(2.8, 0.02), 'LP5907MFX-2.8/NOPB' 'Package_TO_SOT_SMD:SOT-23-5',, 'C186700'), - # (Range.from_tolerance(2.85, 0.02), 'LP5907MFX-2.85/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C2877840'), # zero stock JLC - # (Range.from_tolerance(2.9, 0.02), 'LP5907MFX-2.9/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', None), - (Range.from_tolerance(3.0, 0.02), 'LP5907MFX-3.0/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C475492'), - # (Range.from_tolerance(3.1, 0.02), 'LP5907MFX-3.1/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', None), - # (Range.from_tolerance(3.2, 0.02), 'LP5907MFX-3.2/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', None), - (Range.from_tolerance(3.3, 0.02), 'LP5907MFX-3.3/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C80670'), - (Range.from_tolerance(4.5, 0.02), 'LP5907MFX-4.5/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C529554'), - - (Range.from_tolerance(1.8, 0.02), 'LP5907SNX-1.8/NOPB', 'Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm', 'C139378'), - (Range.from_tolerance(2.5, 0.02), 'LP5907SNX-2.5/NOPB', 'Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm', 'C133571'), - (Range.from_tolerance(2.9, 0.02), 'LP5907SNX-2.9/NOPB', 'Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm', 'C2870726'), - (Range.from_tolerance(3.3, 0.02), 'LP5907SNX-3.3/NOPB', 'Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm', 'C133572'), - ] - # TODO should prefer parts by closeness to nominal (center) specified voltage - output_voltage_spec = self.get(self.output_voltage) - footprint_spec = self.get(self.footprint_spec) - suitable_parts = [part for part in parts - if part[0] in output_voltage_spec - and (not footprint_spec or footprint_spec == part[2])] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage, part_number, footprint, jlc_number = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage) - if footprint == 'Package_TO_SOT_SMD:SOT-23-5': - pinning: Dict[str, CircuitPort] = { - '1': self.pwr_in, - '2': self.gnd, - '3': self.en, - # pin 4 is NC - '5': self.pwr_out, - } - elif footprint == 'Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm': - pinning = { - '4': self.pwr_in, - '2': self.gnd, - '3': self.en, - '1': self.pwr_out, - '5': self.gnd, # optionally grounded for better thermal performance - } - else: - raise ValueError - - self.footprint( - 'U', footprint, - pinning, - mfr='Texas Instruments', part=part_number, - datasheet='https://www.ti.com/lit/ds/symlink/lp5907.pdf', - ) - self.assign(self.lcsc_part, jlc_number) - self.assign(self.actual_basic_part, False) + """AP2210 RF ULDO in SOT-23-5 with high PSRR and high(er) voltage tolerant.""" + + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.en + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Ap2210_Device(output_voltage=self.output_voltage)) + self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) + self.out_cap = imp.Block(DecouplingCapacitor(capacitance=2.2 * uFarad(tol=0.2))) + + self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) + self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) + + +class Lp5907_Device( + InternalSubcircuit, + LinearRegulatorDevice, + SelectorFootprint, + PartsTablePart, + JlcPart, + GeneratorBlock, + FootprintBlock, +): + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.assign(self.pwr_in.voltage_limits, (2.2, 5.5) * Volt) + self.assign(self.pwr_out.current_limits, (0, 250) * mAmp) + self.assign(self.actual_quiescent_current, (0.2, 425) * uAmp) + self.assign(self.actual_dropout, (50, 250) * mVolt) + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage, self.part, self.footprint_spec) + + self.en = self.Port( + DigitalSink( + voltage_limits=(0, 5.5) * Volt, current_draw=(0.001, 5.5) * uAmp, input_thresholds=(0.4, 1.2) * Volt + ) + ) + + @override + def generate(self) -> None: + super().generate() + parts = [ # output voltage, Table in 6.5 tolerance varies by output voltage + (Range.from_tolerance(1.2, 0.03), "LP5907MFX-1.2/NOPB", "Package_TO_SOT_SMD:SOT-23-5", "C73478"), + (Range.from_tolerance(1.5, 0.03), "LP5907MFX-1.5/NOPB", "Package_TO_SOT_SMD:SOT-23-5", "C133570"), + (Range.from_tolerance(1.8, 0.02), "LP5907MFX-1.8/NOPB", "Package_TO_SOT_SMD:SOT-23-5", "C92498"), + (Range.from_tolerance(2.5, 0.02), "LP5907MFX-2.5/NOPB", "Package_TO_SOT_SMD:SOT-23-5", "C165132"), + # (Range.from_tolerance(2.8, 0.02), 'LP5907MFX-2.8/NOPB' 'Package_TO_SOT_SMD:SOT-23-5',, 'C186700'), + # (Range.from_tolerance(2.85, 0.02), 'LP5907MFX-2.85/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', 'C2877840'), # zero stock JLC + # (Range.from_tolerance(2.9, 0.02), 'LP5907MFX-2.9/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', None), + (Range.from_tolerance(3.0, 0.02), "LP5907MFX-3.0/NOPB", "Package_TO_SOT_SMD:SOT-23-5", "C475492"), + # (Range.from_tolerance(3.1, 0.02), 'LP5907MFX-3.1/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', None), + # (Range.from_tolerance(3.2, 0.02), 'LP5907MFX-3.2/NOPB', 'Package_TO_SOT_SMD:SOT-23-5', None), + (Range.from_tolerance(3.3, 0.02), "LP5907MFX-3.3/NOPB", "Package_TO_SOT_SMD:SOT-23-5", "C80670"), + (Range.from_tolerance(4.5, 0.02), "LP5907MFX-4.5/NOPB", "Package_TO_SOT_SMD:SOT-23-5", "C529554"), + ( + Range.from_tolerance(1.8, 0.02), + "LP5907SNX-1.8/NOPB", + "Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm", + "C139378", + ), + ( + Range.from_tolerance(2.5, 0.02), + "LP5907SNX-2.5/NOPB", + "Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm", + "C133571", + ), + ( + Range.from_tolerance(2.9, 0.02), + "LP5907SNX-2.9/NOPB", + "Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm", + "C2870726", + ), + ( + Range.from_tolerance(3.3, 0.02), + "LP5907SNX-3.3/NOPB", + "Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm", + "C133572", + ), + ] + # TODO should prefer parts by closeness to nominal (center) specified voltage + output_voltage_spec = self.get(self.output_voltage) + footprint_spec = self.get(self.footprint_spec) + suitable_parts = [ + part + for part in parts + if part[0] in output_voltage_spec and (not footprint_spec or footprint_spec == part[2]) + ] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage, part_number, footprint, jlc_number = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage) + if footprint == "Package_TO_SOT_SMD:SOT-23-5": + pinning: Dict[str, CircuitPort] = { + "1": self.pwr_in, + "2": self.gnd, + "3": self.en, + # pin 4 is NC + "5": self.pwr_out, + } + elif footprint == "Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm": + pinning = { + "4": self.pwr_in, + "2": self.gnd, + "3": self.en, + "1": self.pwr_out, + "5": self.gnd, # optionally grounded for better thermal performance + } + else: + raise ValueError + + self.footprint( + "U", + footprint, + pinning, + mfr="Texas Instruments", + part=part_number, + datasheet="https://www.ti.com/lit/ds/symlink/lp5907.pdf", + ) + self.assign(self.lcsc_part, jlc_number) + self.assign(self.actual_basic_part, False) class Lp5907(VoltageRegulatorEnableWrapper, LinearRegulator): - """High-PSRR LDO in SOT-23-5. - Other pin-compatible high-PSRR LDOs: - - LP5907 - - AP139 - - TCR2EF - """ - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.en - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Lp5907_Device(output_voltage=self.output_voltage)) - self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) - self.out_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) - - self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) - self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) - - -class Tlv757p_Device(InternalSubcircuit, LinearRegulatorDevice, PartsTablePart, JlcPart, GeneratorBlock, - FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.assign(self.pwr_in.voltage_limits, (1.45, 5.5) * Volt) - self.assign(self.pwr_out.current_limits, (0, 1.2) * Amp) - self.assign(self.actual_quiescent_current, (0.1, 40) * uAmp) # shutdown to worst-case ground current - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage, self.part) - - self.en = self.Port(DigitalSink( - voltage_limits=(0, 5.5) * Volt, - input_thresholds=(0.3, 1)*Volt - )) - - @override - def generate(self) -> None: - super().generate() - tolerance = 0.015 # over 125C range, Vout >= 1v - parts = [ # output range, part, dropout voltage for non-DYD over 125C range, LCSC - (Range.from_tolerance(1.0, tolerance), 'TLV75710PDBVR', 1.2, 'C2865385'), - (Range.from_tolerance(1.2, tolerance), 'TLV75712PDBVR', 1.1, 'C1509296'), - (Range.from_tolerance(1.5, tolerance), 'TLV75715PDBVR', 0.85, 'C2872562'), - (Range.from_tolerance(1.8, tolerance), 'TLV75718PDBVR', 0.8, 'C507270'), - (Range.from_tolerance(1.9, tolerance), 'TLV75719PDBVR', 0.8, 'C2868954'), - (Range.from_tolerance(2.5, tolerance), 'TLV75725PDBVR', 0.65, 'C2872563'), - (Range.from_tolerance(2.8, tolerance), 'TLV75728PDBVR', 0.65, 'C2863639'), - (Range.from_tolerance(2.9, tolerance), 'TLV75729PDBVR', 0.65, 'C2861884'), - (Range.from_tolerance(3.0, tolerance), 'TLV75730PDBVR', 0.65, 'C2872564'), - (Range.from_tolerance(3.3, tolerance), 'TLV75733PDBVR', 0.475, 'C485517'), - ] - - # TODO should prefer parts by closeness to nominal (center) specified voltage - output_voltage_spec = self.get(self.output_voltage) - suitable_parts = [part for part in parts - if part[0] in output_voltage_spec] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage, part_number, part_dropout, lcsc = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage) - self.assign(self.actual_dropout, (0, part_dropout)*Volt) - - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.pwr_in, - '2': self.gnd, - '3': self.en, - # pin 4 is NC - '5': self.pwr_out, - }, - mfr='Texas Instruments', part=part_number, - datasheet='https://www.ti.com/lit/ds/symlink/tlv757p.pdf', - ) - self.assign(self.lcsc_part, lcsc) - self.assign(self.actual_basic_part, False) + """High-PSRR LDO in SOT-23-5. + Other pin-compatible high-PSRR LDOs: + - LP5907 + - AP139 + - TCR2EF + """ + + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.en + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Lp5907_Device(output_voltage=self.output_voltage)) + self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) + self.out_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) + + self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) + self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) + + +class Tlv757p_Device( + InternalSubcircuit, LinearRegulatorDevice, PartsTablePart, JlcPart, GeneratorBlock, FootprintBlock +): + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.assign(self.pwr_in.voltage_limits, (1.45, 5.5) * Volt) + self.assign(self.pwr_out.current_limits, (0, 1.2) * Amp) + self.assign(self.actual_quiescent_current, (0.1, 40) * uAmp) # shutdown to worst-case ground current + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage, self.part) + + self.en = self.Port(DigitalSink(voltage_limits=(0, 5.5) * Volt, input_thresholds=(0.3, 1) * Volt)) + + @override + def generate(self) -> None: + super().generate() + tolerance = 0.015 # over 125C range, Vout >= 1v + parts = [ # output range, part, dropout voltage for non-DYD over 125C range, LCSC + (Range.from_tolerance(1.0, tolerance), "TLV75710PDBVR", 1.2, "C2865385"), + (Range.from_tolerance(1.2, tolerance), "TLV75712PDBVR", 1.1, "C1509296"), + (Range.from_tolerance(1.5, tolerance), "TLV75715PDBVR", 0.85, "C2872562"), + (Range.from_tolerance(1.8, tolerance), "TLV75718PDBVR", 0.8, "C507270"), + (Range.from_tolerance(1.9, tolerance), "TLV75719PDBVR", 0.8, "C2868954"), + (Range.from_tolerance(2.5, tolerance), "TLV75725PDBVR", 0.65, "C2872563"), + (Range.from_tolerance(2.8, tolerance), "TLV75728PDBVR", 0.65, "C2863639"), + (Range.from_tolerance(2.9, tolerance), "TLV75729PDBVR", 0.65, "C2861884"), + (Range.from_tolerance(3.0, tolerance), "TLV75730PDBVR", 0.65, "C2872564"), + (Range.from_tolerance(3.3, tolerance), "TLV75733PDBVR", 0.475, "C485517"), + ] + + # TODO should prefer parts by closeness to nominal (center) specified voltage + output_voltage_spec = self.get(self.output_voltage) + suitable_parts = [part for part in parts if part[0] in output_voltage_spec] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage, part_number, part_dropout, lcsc = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage) + self.assign(self.actual_dropout, (0, part_dropout) * Volt) + + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.pwr_in, + "2": self.gnd, + "3": self.en, + # pin 4 is NC + "5": self.pwr_out, + }, + mfr="Texas Instruments", + part=part_number, + datasheet="https://www.ti.com/lit/ds/symlink/tlv757p.pdf", + ) + self.assign(self.lcsc_part, lcsc) + self.assign(self.actual_basic_part, False) class Tlv757p(VoltageRegulatorEnableWrapper, LinearRegulator): - """1A LDO regulator in SOT-23-5 with 1.45-5.5Vin and 0.5-5Vout - By default, this models worst-case dropout at 1A, consult datasheet for lower dropouts for lower currents - While it can electrically handle 1A, beware of thermal limits. Shuts down at 155C, Rja for SOT-23-5 of 100C/W - """ - @override - def _generator_inner_reset_pin(self) -> Port[DigitalLink]: - return self.ic.en - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( + """1A LDO regulator in SOT-23-5 with 1.45-5.5Vin and 0.5-5Vout + By default, this models worst-case dropout at 1A, consult datasheet for lower dropouts for lower currents + While it can electrically handle 1A, beware of thermal limits. Shuts down at 155C, Rja for SOT-23-5 of 100C/W + """ + + @override + def _generator_inner_reset_pin(self) -> Port[DigitalLink]: + return self.ic.en + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect( ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Tlv757p_Device(output_voltage=self.output_voltage)) - self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) - self.out_cap = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2))) + ) as imp: + self.ic = imp.Block(Tlv757p_Device(output_voltage=self.output_voltage)) + self.in_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) + self.out_cap = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))) - self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) - self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) + self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) + self.connect(self.pwr_out, self.ic.pwr_out, self.out_cap.pwr) class L78l_Device(InternalSubcircuit, LinearRegulatorDevice, JlcPart, GeneratorBlock, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage) - - @override - def generate(self) -> None: - super().generate() - - parts = [ # output voltage, input max voltage, quiescent current, dropout - (Range(3.135, 3.465), (0, 30)*Volt, (5.5, 6)*mAmp, (0, 2)*Volt, 'L78L33AC', 'C43116'), - (Range(4.75, 5.25), (0, 30)*Volt, (5.5, 6)*mAmp, (0, 2)*Volt, 'L78L05AC', 'C43116'), - (Range(5.7, 6.3), (0, 30)*Volt, (5.5, 6)*mAmp, (0, 1.7)*Volt, 'L78L06AC', 'C81357'), - (Range(7.6, 8.4), (0, 30)*Volt, (5.5, 6)*mAmp, (0, 1.7)*Volt, 'L78L08AC', 'C39490'), # low stock - (Range(8.55, 9.45), (0, 30)*Volt, (5.5, 6)*mAmp, (0, 1.7)*Volt, 'L78L09AC', 'C377943'), # out of stock - (Range(9.5, 10.5), (0, 30)*Volt, (5.5, 6)*mAmp, (0, 1.7)*Volt, 'L78L10AC', 'C222250'), - (Range(11.5, 12.5), (0, 35)*Volt, (6, 6.5)*mAmp, (0, 1.7)*Volt, 'L78L12AC', 'C69601'), - (Range(14.4, 15.6), (0, 35)*Volt, (6, 6.5)*mAmp, (0, 1.7)*Volt, 'L78L15AC', 'C115285'), - (Range(17.1, 18.9), (0, 40)*Volt, (6, 6.5)*mAmp, (0, 1.7)*Volt, 'L78L18AC', 'C2802523'), # out of stock - (Range(22.8, 25.2), (0, 40)*Volt, (6, 6.5)*mAmp, (0, 1.7)*Volt, 'L78L24AC', 'C130141'), # low stock - ] - suitable_parts = [part for part in parts - if part[0] in self.get(self.output_voltage)] - assert suitable_parts, "no regulator with compatible output" - - self.assign(self.pwr_out.voltage_out, suitable_parts[0][0]) - self.assign(self.pwr_in.voltage_limits, suitable_parts[0][1]) - self.assign(self.pwr_out.current_limits, (0, 100)*mAmp) - self.assign(self.actual_quiescent_current, suitable_parts[0][2]) - self.assign(self.actual_dropout, suitable_parts[0][3]) - - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-89-3', - { - '1': self.pwr_out, - '2': self.gnd, - '3': self.pwr_in, - }, - mfr='STMicroelectronics', part=suitable_parts[0][4], - datasheet='https://www.st.com/resource/en/datasheet/l78l.pdf' - ) - self.assign(self.lcsc_part, suitable_parts[0][5]) - self.assign(self.actual_basic_part, False) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage) + + @override + def generate(self) -> None: + super().generate() + + parts = [ # output voltage, input max voltage, quiescent current, dropout + (Range(3.135, 3.465), (0, 30) * Volt, (5.5, 6) * mAmp, (0, 2) * Volt, "L78L33AC", "C43116"), + (Range(4.75, 5.25), (0, 30) * Volt, (5.5, 6) * mAmp, (0, 2) * Volt, "L78L05AC", "C43116"), + (Range(5.7, 6.3), (0, 30) * Volt, (5.5, 6) * mAmp, (0, 1.7) * Volt, "L78L06AC", "C81357"), + (Range(7.6, 8.4), (0, 30) * Volt, (5.5, 6) * mAmp, (0, 1.7) * Volt, "L78L08AC", "C39490"), # low stock + ( + Range(8.55, 9.45), + (0, 30) * Volt, + (5.5, 6) * mAmp, + (0, 1.7) * Volt, + "L78L09AC", + "C377943", + ), # out of stock + (Range(9.5, 10.5), (0, 30) * Volt, (5.5, 6) * mAmp, (0, 1.7) * Volt, "L78L10AC", "C222250"), + (Range(11.5, 12.5), (0, 35) * Volt, (6, 6.5) * mAmp, (0, 1.7) * Volt, "L78L12AC", "C69601"), + (Range(14.4, 15.6), (0, 35) * Volt, (6, 6.5) * mAmp, (0, 1.7) * Volt, "L78L15AC", "C115285"), + ( + Range(17.1, 18.9), + (0, 40) * Volt, + (6, 6.5) * mAmp, + (0, 1.7) * Volt, + "L78L18AC", + "C2802523", + ), # out of stock + (Range(22.8, 25.2), (0, 40) * Volt, (6, 6.5) * mAmp, (0, 1.7) * Volt, "L78L24AC", "C130141"), # low stock + ] + suitable_parts = [part for part in parts if part[0] in self.get(self.output_voltage)] + assert suitable_parts, "no regulator with compatible output" + + self.assign(self.pwr_out.voltage_out, suitable_parts[0][0]) + self.assign(self.pwr_in.voltage_limits, suitable_parts[0][1]) + self.assign(self.pwr_out.current_limits, (0, 100) * mAmp) + self.assign(self.actual_quiescent_current, suitable_parts[0][2]) + self.assign(self.actual_dropout, suitable_parts[0][3]) + + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-89-3", + { + "1": self.pwr_out, + "2": self.gnd, + "3": self.pwr_in, + }, + mfr="STMicroelectronics", + part=suitable_parts[0][4], + datasheet="https://www.st.com/resource/en/datasheet/l78l.pdf", + ) + self.assign(self.lcsc_part, suitable_parts[0][5]) + self.assign(self.actual_basic_part, False) class L78l(LinearRegulator): - """L78Lxx high(er) input voltage linear regulator in SOT-89. - """ - @override - def contents(self) -> None: - super().contents() - self.ic = self.Block(L78l_Device(self.output_voltage)) - self.connect(self.pwr_in, self.ic.pwr_in) - self.connect(self.pwr_out, self.ic.pwr_out) - self.connect(self.gnd, self.ic.gnd) - self.in_cap = self.Block(DecouplingCapacitor(capacitance=0.33*uFarad(tol=0.2))) \ - .connected(self.gnd, self.pwr_in) - self.out_cap = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2))) \ - .connected(self.gnd, self.pwr_out) + """L78Lxx high(er) input voltage linear regulator in SOT-89.""" + + @override + def contents(self) -> None: + super().contents() + self.ic = self.Block(L78l_Device(self.output_voltage)) + self.connect(self.pwr_in, self.ic.pwr_in) + self.connect(self.pwr_out, self.ic.pwr_out) + self.connect(self.gnd, self.ic.gnd) + self.in_cap = self.Block(DecouplingCapacitor(capacitance=0.33 * uFarad(tol=0.2))).connected( + self.gnd, self.pwr_in + ) + self.out_cap = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected( + self.gnd, self.pwr_out + ) diff --git a/edg/parts/Logic_74Ahct.py b/edg/parts/Logic_74Ahct.py index c065c486d..1969c1c85 100644 --- a/edg/parts/Logic_74Ahct.py +++ b/edg/parts/Logic_74Ahct.py @@ -5,53 +5,66 @@ class L74Ahct1g125_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground()) - self.vcc = self.Port(VoltageSink( - voltage_limits=(4.5, 5.5)*Volt, - current_draw=(1, 40)*uAmp # typ to max, TODO propagate current draw from loads - )) - # TODO optional OE pin - self.a = self.Port(DigitalSink.from_supply( - self.gnd, self.vcc, - voltage_limit_tolerance=(-0.5, 0.5)*Volt, - input_threshold_abs=(0.8, 2.0)*Volt, - ), [Input]) - self.y = self.Port(DigitalSource.from_supply( - self.gnd, self.vcc, - current_limits=(-8, 8)*mAmp, - ), [Output]) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.gnd, # OE - '2': self.a, - '3': self.gnd, - '4': self.y, - '5': self.vcc, - }, - mfr='Diodes Incorporated', part='74AHCT1G125W5-7', - datasheet='https://www.diodes.com/assets/Datasheets/74AHCT1G125.pdf' - ) - self.assign(self.lcsc_part, 'C842287') + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground()) + self.vcc = self.Port( + VoltageSink( + voltage_limits=(4.5, 5.5) * Volt, + current_draw=(1, 40) * uAmp, # typ to max, TODO propagate current draw from loads + ) + ) + # TODO optional OE pin + self.a = self.Port( + DigitalSink.from_supply( + self.gnd, + self.vcc, + voltage_limit_tolerance=(-0.5, 0.5) * Volt, + input_threshold_abs=(0.8, 2.0) * Volt, + ), + [Input], + ) + self.y = self.Port( + DigitalSource.from_supply( + self.gnd, + self.vcc, + current_limits=(-8, 8) * mAmp, + ), + [Output], + ) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.gnd, # OE + "2": self.a, + "3": self.gnd, + "4": self.y, + "5": self.vcc, + }, + mfr="Diodes Incorporated", + part="74AHCT1G125W5-7", + datasheet="https://www.diodes.com/assets/Datasheets/74AHCT1G125.pdf", + ) + self.assign(self.lcsc_part, "C842287") class L74Ahct1g125(Interface, Block): - """Single buffer, useful as a level shifter""" - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(L74Ahct1g125_Device()) - self.pwr = self.Export(self.ic.vcc, [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.input = self.Export(self.ic.a, [Input]) - self.output = self.Export(self.ic.y, [Output]) - - @override - def contents(self) -> None: - super().contents() - self.vdd_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + """Single buffer, useful as a level shifter""" + + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(L74Ahct1g125_Device()) + self.pwr = self.Export(self.ic.vcc, [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.input = self.Export(self.ic.a, [Input]) + self.output = self.Export(self.ic.y, [Output]) + + @override + def contents(self) -> None: + super().contents() + self.vdd_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/Logic_74Lvc.py b/edg/parts/Logic_74Lvc.py index 8c8095948..9db936233 100644 --- a/edg/parts/Logic_74Lvc.py +++ b/edg/parts/Logic_74Lvc.py @@ -8,26 +8,24 @@ class Sn74lvc1g74_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vcc = self.Port(VoltageSink( - voltage_limits=(1.65, 5.5)*Volt, - current_draw=(0, 10)*uAmp # Icc - )) + self.vcc = self.Port(VoltageSink(voltage_limits=(1.65, 5.5) * Volt, current_draw=(0, 10) * uAmp)) # Icc din_model = DigitalSink.from_supply( - self.gnd, self.vcc, - voltage_limit_abs=(-0.5, 6.5)*Volt, - input_threshold_factor=(0.3, 0.7) # tightest over operating range + self.gnd, + self.vcc, + voltage_limit_abs=(-0.5, 6.5) * Volt, + input_threshold_factor=(0.3, 0.7), # tightest over operating range ) self.clk = self.Port(din_model, optional=True) self.d = self.Port(din_model, optional=True) self.nclr = self.Port(din_model, optional=True) self.npre = self.Port(din_model, optional=True) self.require(self.d.is_connected() == self.clk.is_connected()) - self.require(self.d.is_connected() | self.clk.is_connected() | - self.nclr.is_connected() | self.npre.is_connected()) + self.require( + self.d.is_connected() | self.clk.is_connected() | self.nclr.is_connected() | self.npre.is_connected() + ) dout_model = DigitalSource.from_supply( - self.gnd, self.vcc, - current_limits=(-4, 4)*mAmp # for Vcc=1.65V, increases with higher Vcc + self.gnd, self.vcc, current_limits=(-4, 4) * mAmp # for Vcc=1.65V, increases with higher Vcc ) self.q = self.Port(dout_model, optional=True) self.nq = self.Port(dout_model, optional=True) @@ -37,27 +35,30 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() self.footprint( - 'U', 'Package_SO:VSSOP-8_2.4x2.1mm_P0.5mm', + "U", + "Package_SO:VSSOP-8_2.4x2.1mm_P0.5mm", { - '1': self.clk, - '2': self.d, - '3': self.nq, - '4': self.gnd, - '5': self.q, - '6': self.nclr, - '7': self.npre, - '8': self.vcc, + "1": self.clk, + "2": self.d, + "3": self.nq, + "4": self.gnd, + "5": self.q, + "6": self.nclr, + "7": self.npre, + "8": self.vcc, }, - mfr='Texas Instruments', part='SN74LVC1G74DCUR', - datasheet='https://www.ti.com/lit/ds/symlink/sn74lvc1g74.pdf' + mfr="Texas Instruments", + part="SN74LVC1G74DCUR", + datasheet="https://www.ti.com/lit/ds/symlink/sn74lvc1g74.pdf", ) - self.assign(self.lcsc_part, 'C70285') + self.assign(self.lcsc_part, "C70285") class Sn74lvc1g74(Interface, Block): """D flip-flop with clear and preset TODO: should extend an abstract flip-lop interface, with async (n)set and (n)clear mixins""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Sn74lvc1g74_Device()) @@ -73,22 +74,20 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.vdd_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) class Sn74lvc2g02_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vcc = self.Port(VoltageSink( - voltage_limits=(1.65, 5.5)*Volt, - current_draw=(0, 10)*uAmp # Icc - )) + self.vcc = self.Port(VoltageSink(voltage_limits=(1.65, 5.5) * Volt, current_draw=(0, 10) * uAmp)) # Icc din_model = DigitalSink.from_supply( - self.gnd, self.vcc, - voltage_limit_abs=(-0.5, 6.5)*Volt, - input_threshold_factor=(0.3, 0.7) # tightest over operating range + self.gnd, + self.vcc, + voltage_limit_abs=(-0.5, 6.5) * Volt, + input_threshold_factor=(0.3, 0.7), # tightest over operating range ) self.in1a = self.Port(din_model, optional=True) self.in1b = self.Port(din_model, optional=True) @@ -96,41 +95,47 @@ def __init__(self) -> None: self.in2b = self.Port(din_model, optional=True) dout_model = DigitalSource.from_supply( - self.gnd, self.vcc, - current_limits=(-4, 4)*mAmp # for Vcc=1.65V, increases with higher Vcc + self.gnd, self.vcc, current_limits=(-4, 4) * mAmp # for Vcc=1.65V, increases with higher Vcc ) self.out1 = self.Port(dout_model, optional=True) self.out2 = self.Port(dout_model, optional=True) - self.require((self.out1.is_connected() == self.in1a.is_connected()) & - (self.out1.is_connected() == self.in1b.is_connected())) - self.require((self.out2.is_connected() == self.in2a.is_connected()) & - (self.out2.is_connected() == self.in2b.is_connected())) + self.require( + (self.out1.is_connected() == self.in1a.is_connected()) + & (self.out1.is_connected() == self.in1b.is_connected()) + ) + self.require( + (self.out2.is_connected() == self.in2a.is_connected()) + & (self.out2.is_connected() == self.in2b.is_connected()) + ) @override def contents(self) -> None: super().contents() self.footprint( - 'U', 'Package_SO:VSSOP-8_2.3x2mm_P0.5mm', + "U", + "Package_SO:VSSOP-8_2.3x2mm_P0.5mm", { - '1': self.in1a, - '2': self.in1b, - '3': self.out2, - '4': self.gnd, - '5': self.in2a, - '6': self.in2b, - '7': self.out1, - '8': self.vcc, + "1": self.in1a, + "2": self.in1b, + "3": self.out2, + "4": self.gnd, + "5": self.in2a, + "6": self.in2b, + "7": self.out1, + "8": self.vcc, }, - mfr='Texas Instruments', part='SN74LVC2G02DCUR', - datasheet='https://www.ti.com/lit/ds/symlink/sn74lvc2g02.pdf' + mfr="Texas Instruments", + part="SN74LVC2G02DCUR", + datasheet="https://www.ti.com/lit/ds/symlink/sn74lvc2g02.pdf", ) - self.assign(self.lcsc_part, 'C133589') + self.assign(self.lcsc_part, "C133589") class Sn74lvc2g02(Interface, Block): """2-input positive NOR gate TODO: support multipacking""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Sn74lvc2g02_Device()) @@ -146,4 +151,4 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.vdd_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/Lsm6dsv16x.py b/edg/parts/Lsm6dsv16x.py index 6b6bf3248..e77abd206 100644 --- a/edg/parts/Lsm6dsv16x.py +++ b/edg/parts/Lsm6dsv16x.py @@ -7,21 +7,22 @@ class Lsm6dsv16x_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(1.71, 3.6) * Volt, - current_draw=(2.6 * uAmp, 0.65 * mAmp) # Values for low-power and high-performance modes - )) - self.vddio = self.Port(VoltageSink( - voltage_limits=(1.08, 3.6) * Volt # Extended range - )) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(1.71, 3.6) * Volt, + current_draw=(2.6 * uAmp, 0.65 * mAmp), # Values for low-power and high-performance modes + ) + ) + self.vddio = self.Port(VoltageSink(voltage_limits=(1.08, 3.6) * Volt)) # Extended range self.gnd = self.Port(Ground()) dio_model = DigitalBidir.from_supply( - self.gnd, self.vddio, + self.gnd, + self.vddio, voltage_limit_abs=(-0.3 * Volt, self.vddio.voltage_limits.upper() + 0.3), # datasheet states abs volt to be [0.3, VDD_IO+0.3], likely a typo current_limits=(-4, 4) * mAmp, - input_threshold_factor=(0.3, 0.7) + input_threshold_factor=(0.3, 0.7), ) self.i2c = self.Port(I2cTarget(dio_model)) @@ -35,27 +36,29 @@ def __init__(self) -> None: @override def contents(self) -> None: self.footprint( - 'U', 'Package_LGA:Bosch_LGA-14_3x2.5mm_P0.5mm', + "U", + "Package_LGA:Bosch_LGA-14_3x2.5mm_P0.5mm", { - '1': self.gnd, # sa0 - '2': self.qvar1, # not used in mode 1 - '3': self.qvar2, # not used in mode 1 - '4': self.int1, - '5': self.vddio, - '6': self.gnd, - '7': self.gnd, - '8': self.vdd, - '9': self.int2, + "1": self.gnd, # sa0 + "2": self.qvar1, # not used in mode 1 + "3": self.qvar2, # not used in mode 1 + "4": self.int1, + "5": self.vddio, + "6": self.gnd, + "7": self.gnd, + "8": self.vdd, + "9": self.int2, # '10': self.nc, # leave unconnected # '11': self.nc, # leave unconnected - '12': self.vddio, # cs pin - '13': self.i2c.scl, - '14': self.i2c.sda, + "12": self.vddio, # cs pin + "13": self.i2c.scl, + "14": self.i2c.sda, }, - mfr='STMicroelectronics', part='LSM6DSV16X', - datasheet='https://www.st.com/resource/en/datasheet/lsm6dsv16x.pdf' + mfr="STMicroelectronics", + part="LSM6DSV16X", + datasheet="https://www.st.com/resource/en/datasheet/lsm6dsv16x.pdf", ) - self.assign(self.lcsc_part, 'C5267406') + self.assign(self.lcsc_part, "C5267406") self.assign(self.actual_basic_part, False) @@ -72,10 +75,12 @@ def __init__(self) -> None: self.pwr_io = self.Export(self.ic.vddio, default=self.pwr, doc="IO supply voltage") self.i2c = self.Export(self.ic.i2c) - self.int1 = self.Export(self.ic.int1, optional=True, - doc="Programmable interrupt. This can be configured as push-pull / open-drain.") - self.int2 = self.Export(self.ic.int2, optional=True, - doc="Programmable interrupt. This can be configured as push-pull / open-drain.") + self.int1 = self.Export( + self.ic.int1, optional=True, doc="Programmable interrupt. This can be configured as push-pull / open-drain." + ) + self.int2 = self.Export( + self.ic.int2, optional=True, doc="Programmable interrupt. This can be configured as push-pull / open-drain." + ) self.qvar1 = self.Export(self.ic.qvar1, optional=True, doc="qvar input pin 1") self.qvar2 = self.Export(self.ic.qvar2, optional=True, doc="qvar input pin 2") diff --git a/edg/parts/Mag_Qmc5883l.py b/edg/parts/Mag_Qmc5883l.py index 2e7d9ab80..0a03cb646 100644 --- a/edg/parts/Mag_Qmc5883l.py +++ b/edg/parts/Mag_Qmc5883l.py @@ -7,19 +7,20 @@ class Qmc5883l_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.16, 3.6)*Volt, - current_draw=(3*uAmp, 2.6*mAmp) # standby to peak active, assuming it's all on Vdd - )) - self.vddio = self.Port(VoltageSink( - voltage_limits=(1.65, 3.6)*Volt - )) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(2.16, 3.6) * Volt, + current_draw=(3 * uAmp, 2.6 * mAmp), # standby to peak active, assuming it's all on Vdd + ) + ) + self.vddio = self.Port(VoltageSink(voltage_limits=(1.65, 3.6) * Volt)) self.gnd = self.Port(Ground()) dio_model = DigitalBidir.from_supply( - self.gnd, self.vddio, - voltage_limit_abs=(-0.3*Volt, self.vddio.voltage_limits.upper()+0.3), - input_threshold_factor=(0.3, 0.7) + self.gnd, + self.vddio, + voltage_limit_abs=(-0.3 * Volt, self.vddio.voltage_limits.upper() + 0.3), + input_threshold_factor=(0.3, 0.7), ) self.i2c = self.Port(I2cTarget(dio_model)) self.drdy = self.Port(DigitalSource.from_bidir(dio_model), optional=True) @@ -31,29 +32,31 @@ def __init__(self) -> None: @override def contents(self) -> None: self.footprint( - 'U', 'Package_LGA:LGA-16_3x3mm_P0.5mm', + "U", + "Package_LGA:LGA-16_3x3mm_P0.5mm", { - '1': self.i2c.scl, - '2': self.vdd, + "1": self.i2c.scl, + "2": self.vdd, # '3': NC - '4': self.vddio, # S1, "tie top VddIO + "4": self.vddio, # S1, "tie top VddIO # '5': NC # '6': NC # '7': NC - '8': self.setp, - '9': self.gnd, - '10': self.c1, - '11': self.gnd, - '12': self.setc, - '13': self.vddio, + "8": self.setp, + "9": self.gnd, + "10": self.c1, + "11": self.gnd, + "12": self.setc, + "13": self.vddio, # '14': NC - '15': self.drdy, - '16': self.i2c.sda, + "15": self.drdy, + "16": self.i2c.sda, }, - mfr='QST', part='QMC5883L', - datasheet='https://www.filipeflop.com/img/files/download/Datasheet-QMC5883L-1.0%20.pdf' # first result on Google + mfr="QST", + part="QMC5883L", + datasheet="https://www.filipeflop.com/img/files/download/Datasheet-QMC5883L-1.0%20.pdf", # first result on Google ) - self.assign(self.lcsc_part, 'C976032') + self.assign(self.lcsc_part, "C976032") self.assign(self.actual_basic_part, False) @@ -61,6 +64,7 @@ class Qmc5883l(Magnetometer, DefaultExportBlock): """3-axis magnetometer. This part seems to be a licensed semi-copy of the HMC5883L which is no longer in production. It might be hardware drop-in compatible though the firmware protocol differs.""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Qmc5883l_Device()) @@ -73,10 +77,10 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.vdd_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) - self.set_cap = self.Block(Capacitor(0.22*uFarad(tol=0.2), voltage=self.pwr.link().voltage)) + self.vdd_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + self.set_cap = self.Block(Capacitor(0.22 * uFarad(tol=0.2), voltage=self.pwr.link().voltage)) self.connect(self.set_cap.pos, self.ic.setp) self.connect(self.set_cap.neg, self.ic.setc) - self.c1 = self.Block(Capacitor(4.7*uFarad(tol=0.2), voltage=self.pwr.link().voltage)) + self.c1 = self.Block(Capacitor(4.7 * uFarad(tol=0.2), voltage=self.pwr.link().voltage)) self.connect(self.c1.pos, self.ic.c1) self.connect(self.c1.neg.adapt_to(Ground()), self.gnd) diff --git a/edg/parts/MagneticSensor_A1304.py b/edg/parts/MagneticSensor_A1304.py index c0667792e..057f5dbc8 100644 --- a/edg/parts/MagneticSensor_A1304.py +++ b/edg/parts/MagneticSensor_A1304.py @@ -8,35 +8,42 @@ class A1304_Device(InternalBlock, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vcc = self.Port(VoltageSink.from_gnd( - self.gnd, - voltage_limits=(3, 3.6)*Volt, - current_draw=(7.7, 9)*mAmp, # typ to max - )) - self.vout = self.Port(AnalogSource.from_supply( - self.gnd, self.vcc, - signal_out_abs=(0.38, 2.87) # output saturation limits @ Vcc=3.3v - )) + self.vcc = self.Port( + VoltageSink.from_gnd( + self.gnd, + voltage_limits=(3, 3.6) * Volt, + current_draw=(7.7, 9) * mAmp, # typ to max + ) + ) + self.vout = self.Port( + AnalogSource.from_supply( + self.gnd, self.vcc, signal_out_abs=(0.38, 2.87) # output saturation limits @ Vcc=3.3v + ) + ) @override def contents(self) -> None: self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', + "U", + "Package_TO_SOT_SMD:SOT-23", { - '1': self.vcc, - '2': self.vout, - '3': self.gnd, + "1": self.vcc, + "2": self.vout, + "3": self.gnd, }, - mfr="Allegro MicroSystems", part='A1304ELHLX-T', - datasheet='https://www.allegromicro.com/~/media/Files/Datasheets/A1304-Datasheet.ashx') + mfr="Allegro MicroSystems", + part="A1304ELHLX-T", + datasheet="https://www.allegromicro.com/~/media/Files/Datasheets/A1304-Datasheet.ashx", + ) - self.assign(self.lcsc_part, 'C545185') + self.assign(self.lcsc_part, "C545185") self.assign(self.actual_basic_part, False) class A1304(Magnetometer, Block): """Linear hall-effect sensor with analog output, sometimes used in game controllers as trigger detectors. Typ 4 mV / Gauss with full scale range of +/-375 Gauss.""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(A1304_Device()) @@ -47,4 +54,4 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.cbyp = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.cbyp = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/MagneticSwitch_Ah1806.py b/edg/parts/MagneticSwitch_Ah1806.py index deec12ed6..db1b7cad8 100644 --- a/edg/parts/MagneticSwitch_Ah1806.py +++ b/edg/parts/MagneticSwitch_Ah1806.py @@ -8,28 +8,31 @@ class Ah1806_Device(InternalBlock, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vdd = self.Port(VoltageSink.from_gnd( - self.gnd, - voltage_limits=(2.5, 5.5)*Volt, - current_draw=(0, 40)*uAmp, # average, up to 12mA when awake - )) - self.output = self.Port(DigitalSource.low_from_supply( - self.gnd, current_limits=(-1, 0)*mAmp - )) + self.vdd = self.Port( + VoltageSink.from_gnd( + self.gnd, + voltage_limits=(2.5, 5.5) * Volt, + current_draw=(0, 40) * uAmp, # average, up to 12mA when awake + ) + ) + self.output = self.Port(DigitalSource.low_from_supply(self.gnd, current_limits=(-1, 0) * mAmp)) @override def contents(self) -> None: self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', + "U", + "Package_TO_SOT_SMD:SOT-23", { - '1': self.vdd, # note, kicad pin numbers differs from datasheet pin numbers - '3': self.gnd, - '2': self.output, + "1": self.vdd, # note, kicad pin numbers differs from datasheet pin numbers + "3": self.gnd, + "2": self.output, }, - mfr="Diodes Incorporated", part='AH1806-W-7', - datasheet='https://www.diodes.com/assets/Datasheets/AH1806.pdf') + mfr="Diodes Incorporated", + part="AH1806-W-7", + datasheet="https://www.diodes.com/assets/Datasheets/AH1806.pdf", + ) - self.assign(self.lcsc_part, 'C126663') + self.assign(self.lcsc_part, "C126663") self.assign(self.actual_basic_part, False) @@ -39,6 +42,7 @@ class Ah1806(MagneticSwitch, Block): and 20 G (10-40 tolerance range) release point. 0.1% duty cycle, period of 75ms (typ). Pin-compatible with some others in the AH18xx series and DRV5032, which have different trip characteristics""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Ah1806_Device()) @@ -49,4 +53,4 @@ def __init__(self) -> None: @override def contents(self) -> None: super().contents() - self.cin = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.cin = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/Mechanicals.py b/edg/parts/Mechanicals.py index c579358c7..1e210746f 100644 --- a/edg/parts/Mechanicals.py +++ b/edg/parts/Mechanicals.py @@ -3,57 +3,57 @@ from ..abstract_parts import * + @deprecated("non-circuit footprints should be added in layout as non-schematic items") class Outline_Pn1332(Mechanical, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.footprint( - '', 'calisco:Outline_150mm_70mm_PNX-91432', - {}, - mfr='Bud Industries', part='PN-1332-CMB', - datasheet='http://www.budind.com/pdf/hbpn1332.pdf' - ) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "", + "calisco:Outline_150mm_70mm_PNX-91432", + {}, + mfr="Bud Industries", + part="PN-1332-CMB", + datasheet="http://www.budind.com/pdf/hbpn1332.pdf", + ) @abstract_block @deprecated("non-circuit footprints should be added in layout as non-schematic items") class MountingHole(Mechanical, FootprintBlock): - FOOTPRINT: str = '' - VALUE: str = '' - def __init__(self) -> None: - super().__init__() - self.pad = self.Port(Passive(), optional=True) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'H', self.FOOTPRINT, - {}, - value=self.VALUE - ) + FOOTPRINT: str = "" + VALUE: str = "" + + def __init__(self) -> None: + super().__init__() + self.pad = self.Port(Passive(), optional=True) + + @override + def contents(self) -> None: + super().contents() + self.footprint("H", self.FOOTPRINT, {}, value=self.VALUE) @deprecated("non-circuit footprints should be added in layout as non-schematic items") class MountingHole_NoPad_M2_5(MountingHole): - FOOTPRINT = 'MountingHole:MountingHole_2.5mm' - VALUE = 'M2.5' + FOOTPRINT = "MountingHole:MountingHole_2.5mm" + VALUE = "M2.5" @deprecated("non-circuit footprints should be added in layout as non-schematic items") class MountingHole_M2_5(MountingHole): - FOOTPRINT = 'MountingHole:MountingHole_2.7mm_M2.5_Pad_Via' - VALUE = 'M2.5' + FOOTPRINT = "MountingHole:MountingHole_2.7mm_M2.5_Pad_Via" + VALUE = "M2.5" @deprecated("non-circuit footprints should be added in layout as non-schematic items") class MountingHole_M3(MountingHole): - FOOTPRINT = 'MountingHole:MountingHole_3.2mm_M3_Pad_Via' - VALUE = 'M3' + FOOTPRINT = "MountingHole:MountingHole_3.2mm_M3_Pad_Via" + VALUE = "M3" @deprecated("non-circuit footprints should be added in layout as non-schematic items") class MountingHole_M4(MountingHole): - FOOTPRINT = 'MountingHole:MountingHole_4.3mm_M4_Pad_Via' - VALUE = 'M4' + FOOTPRINT = "MountingHole:MountingHole_4.3mm_M4_Pad_Via" + VALUE = "M4" diff --git a/edg/parts/Microcontroller_Esp.py b/edg/parts/Microcontroller_Esp.py index f9a004948..53650d38f 100644 --- a/edg/parts/Microcontroller_Esp.py +++ b/edg/parts/Microcontroller_Esp.py @@ -12,141 +12,147 @@ @abstract_block_default(lambda: EspProgrammingPinHeader254) class EspProgrammingHeader(ProgrammingConnector): - """Abstract programming header for ESP series micros, defining a UART connection. - Circuitry to reset / enter programming mode must be external.""" - def __init__(self) -> None: - super().__init__() + """Abstract programming header for ESP series micros, defining a UART connection. + Circuitry to reset / enter programming mode must be external.""" - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - self.uart = self.Port(UartPort.empty()) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + self.uart = self.Port(UartPort.empty()) class EspProgrammingAutoReset(BlockInterfaceMixin[EspProgrammingHeader]): - """Mixin for ESP programming header with auto-reset and auto-boot pins. - By default, these are required to be connected (since it doesn't make sense to instantiate - this without connecting the additional pins to the micro), but can be disabled with parameters.""" - def __init__(self, *args: Any, require_auto_reset: BoolLike = True, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + """Mixin for ESP programming header with auto-reset and auto-boot pins. + By default, these are required to be connected (since it doesn't make sense to instantiate + this without connecting the additional pins to the micro), but can be disabled with parameters.""" + + def __init__(self, *args: Any, require_auto_reset: BoolLike = True, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) - self.en = self.Port(DigitalSource.empty(), optional=True) # effectively a reset pin - self.boot = self.Port(DigitalSource.empty(), optional=True) # IO0 on ESP32, IO9 on ESP32C3 + self.en = self.Port(DigitalSource.empty(), optional=True) # effectively a reset pin + self.boot = self.Port(DigitalSource.empty(), optional=True) # IO0 on ESP32, IO9 on ESP32C3 - self.require_auto_reset = self.ArgParameter(require_auto_reset) + self.require_auto_reset = self.ArgParameter(require_auto_reset) - @override - def contents(self) -> None: - super().contents() - self.require(self.require_auto_reset.implies(self.en.is_connected() & self.boot.is_connected()), - "auto-reset programming header without auto-reset pins connected, " - "either connect by setting programming='uart-auto' or 'uart-auto-button' on the microcontroller " - "or set require_auto_reset=False to disable this check") + @override + def contents(self) -> None: + super().contents() + self.require( + self.require_auto_reset.implies(self.en.is_connected() & self.boot.is_connected()), + "auto-reset programming header without auto-reset pins connected, " + "either connect by setting programming='uart-auto' or 'uart-auto-button' on the microcontroller " + "or set require_auto_reset=False to disable this check", + ) class EspProgrammingPinHeader254(EspProgrammingHeader): - """Programming header for ESP series micros using 2.54mm headers, matching the pinning in the reference schematics.""" - @override - def contents(self) -> None: - super().contents() + """Programming header for ESP series micros using 2.54mm headers, matching the pinning in the reference schematics.""" - self.conn = self.Block(PinHeader254()) - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - # RXD, TXD reversed to reflect the programmer's side view - self.connect(self.uart.rx, self.conn.pins.request('2').adapt_to(DigitalSink())) - self.connect(self.uart.tx, self.conn.pins.request('3').adapt_to(DigitalSource())) - self.connect(self.gnd, self.conn.pins.request('4').adapt_to(Ground())) + @override + def contents(self) -> None: + super().contents() + + self.conn = self.Block(PinHeader254()) + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + # RXD, TXD reversed to reflect the programmer's side view + self.connect(self.uart.rx, self.conn.pins.request("2").adapt_to(DigitalSink())) + self.connect(self.uart.tx, self.conn.pins.request("3").adapt_to(DigitalSource())) + self.connect(self.gnd, self.conn.pins.request("4").adapt_to(Ground())) class EspProgrammingTc2030(EspProgrammingAutoReset, EspProgrammingHeader): - """UNOFFICIAL tag connect header, based on a modification of the FT232 cable - (https://www.tag-connect.com/product/tc2030-ftdi-ttl-232rg-vsw3v3) - but adding the auto-programming pins (and using DTR instead of CTS into the cable). - Power pins compatible with the official SWD header. + """UNOFFICIAL tag connect header, based on a modification of the FT232 cable + (https://www.tag-connect.com/product/tc2030-ftdi-ttl-232rg-vsw3v3) + but adding the auto-programming pins (and using DTR instead of CTS into the cable). + Power pins compatible with the official SWD header. + + Per boot docs, EN is connected to RTS and boot is connected to DTR (CTS on the original pinning, + since it doesn't have a DTR pin). + """ - Per boot docs, EN is connected to RTS and boot is connected to DTR (CTS on the original pinning, - since it doesn't have a DTR pin). - """ - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.conn = self.Block(TagConnect(6)) - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - self.connect(self.uart.rx, self.conn.pins.request('3').adapt_to(DigitalSink())) - self.connect(self.uart.tx, self.conn.pins.request('4').adapt_to(DigitalSource())) - self.connect(self.gnd, self.conn.pins.request('5').adapt_to(Ground())) + self.conn = self.Block(TagConnect(6)) + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + self.connect(self.uart.rx, self.conn.pins.request("3").adapt_to(DigitalSink())) + self.connect(self.uart.tx, self.conn.pins.request("4").adapt_to(DigitalSource())) + self.connect(self.gnd, self.conn.pins.request("5").adapt_to(Ground())) - # TODO: pulldown is a hack to prevent driver conflict warnings, this should be a active low (open drain) driver - self.connect(self.en, self.conn.pins.request('6').adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) # RTS - self.connect(self.boot, self.conn.pins.request('2').adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) # CTS + # TODO: pulldown is a hack to prevent driver conflict warnings, this should be a active low (open drain) driver + self.connect(self.en, self.conn.pins.request("6").adapt_to(DigitalSource.pulldown_from_supply(self.gnd))) # RTS + self.connect( + self.boot, self.conn.pins.request("2").adapt_to(DigitalSource.pulldown_from_supply(self.gnd)) + ) # CTS @non_library class HasEspProgramming(IoController, GeneratorBlock): - """A mixin for a block (typically an IoController wrapper) that has an ESP style programming header. - Can generate into the standard UART cable, the auto-programming header, or TODO a boundary port.""" - def __init__(self, programming: StringLike = "uart-button"): - super().__init__() - self.programming = self.ArgParameter(programming) # programming connector to generate - self.generator_param(self.programming) - self.program_uart_node = self.connect() - self.program_en_node = self.connect() - self.program_boot_node = self.connect() - - @override - def generate(self) -> None: - super().generate() - programming = self.get(self.programming) - - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.prog = imp.Block(EspProgrammingHeader()) - self.connect(self.program_uart_node, self.prog.uart) - if programming == "uart-button": # default, uart-only header with boot button - self.boot = imp.Block(DigitalSwitch()) - self.connect(self.boot.out, self.program_boot_node) - elif programming == "uart-auto": # UART with auto-programming - auto_prog = self.prog.with_mixin(EspProgrammingAutoReset()) - self.connect(self.program_en_node, auto_prog.en) - self.connect(self.program_boot_node, auto_prog.boot) - elif programming == "uart-auto-button": # both, where the boot button can be used with USB for example - self.boot = imp.Block(DigitalSwitch()) - self.connect(self.boot.out, self.program_boot_node) - auto_prog = self.prog.with_mixin(EspProgrammingAutoReset()) - self.connect(self.program_en_node, auto_prog.en) - self.connect(self.program_boot_node, auto_prog.boot) - else: - self.require(False, "unknown programming connector mode") + """A mixin for a block (typically an IoController wrapper) that has an ESP style programming header. + Can generate into the standard UART cable, the auto-programming header, or TODO a boundary port.""" + + def __init__(self, programming: StringLike = "uart-button"): + super().__init__() + self.programming = self.ArgParameter(programming) # programming connector to generate + self.generator_param(self.programming) + self.program_uart_node = self.connect() + self.program_en_node = self.connect() + self.program_boot_node = self.connect() + + @override + def generate(self) -> None: + super().generate() + programming = self.get(self.programming) + + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.prog = imp.Block(EspProgrammingHeader()) + self.connect(self.program_uart_node, self.prog.uart) + if programming == "uart-button": # default, uart-only header with boot button + self.boot = imp.Block(DigitalSwitch()) + self.connect(self.boot.out, self.program_boot_node) + elif programming == "uart-auto": # UART with auto-programming + auto_prog = self.prog.with_mixin(EspProgrammingAutoReset()) + self.connect(self.program_en_node, auto_prog.en) + self.connect(self.program_boot_node, auto_prog.boot) + elif programming == "uart-auto-button": # both, where the boot button can be used with USB for example + self.boot = imp.Block(DigitalSwitch()) + self.connect(self.boot.out, self.program_boot_node) + auto_prog = self.prog.with_mixin(EspProgrammingAutoReset()) + self.connect(self.program_en_node, auto_prog.en) + self.connect(self.program_boot_node, auto_prog.boot) + else: + self.require(False, "unknown programming connector mode") class EspAutoProgram(Interface, KiCadSchematicBlock): - """Auto-programming circuit for the ESP series, to drive the target EN (reset) and BOOT (e.g., IO0) pins.""" - def __init__(self) -> None: - super().__init__() - self.dtr = self.Port(DigitalSink.empty()) - self.rts = self.Port(DigitalSink.empty()) - - self.en = self.Port(DigitalSource.empty()) - self.boot = self.Port(DigitalSource.empty()) - - @override - def contents(self) -> None: - super().contents() - signal_voltage = self.dtr.link().voltage.hull(self.rts.link().voltage) - signal_thresholds = self.dtr.link().output_thresholds.hull(self.rts.link().output_thresholds) - bjt_model = Bjt(collector_voltage=signal_voltage, collector_current=(0, 0)*Amp, channel='NPN') - self.q_en = self.Block(bjt_model) - self.q_boot = self.Block(bjt_model) - - output_model = DigitalSource(voltage_out=signal_voltage, current_limits=(0, 0)*Amp, # simplified for signal only - output_thresholds=signal_thresholds) - self.import_kicad( - self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'dtr': DigitalSink(), - 'rts': DigitalSink(), - 'en': output_model, - 'boot': output_model - }) + """Auto-programming circuit for the ESP series, to drive the target EN (reset) and BOOT (e.g., IO0) pins.""" + + def __init__(self) -> None: + super().__init__() + self.dtr = self.Port(DigitalSink.empty()) + self.rts = self.Port(DigitalSink.empty()) + + self.en = self.Port(DigitalSource.empty()) + self.boot = self.Port(DigitalSource.empty()) + + @override + def contents(self) -> None: + super().contents() + signal_voltage = self.dtr.link().voltage.hull(self.rts.link().voltage) + signal_thresholds = self.dtr.link().output_thresholds.hull(self.rts.link().output_thresholds) + bjt_model = Bjt(collector_voltage=signal_voltage, collector_current=(0, 0) * Amp, channel="NPN") + self.q_en = self.Block(bjt_model) + self.q_boot = self.Block(bjt_model) + + output_model = DigitalSource( + voltage_out=signal_voltage, + current_limits=(0, 0) * Amp, # simplified for signal only + output_thresholds=signal_thresholds, + ) + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={"dtr": DigitalSink(), "rts": DigitalSink(), "en": output_model, "boot": output_model}, + ) diff --git a/edg/parts/Microcontroller_Esp32.py b/edg/parts/Microcontroller_Esp32.py index 0a9593450..e21ff7166 100644 --- a/edg/parts/Microcontroller_Esp32.py +++ b/edg/parts/Microcontroller_Esp32.py @@ -9,383 +9,424 @@ @non_library -class Esp32_Interfaces(IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerTouchDriver, IoControllerDac, - IoControllerCan, IoControllerDvp8, IoControllerI2s, IoControllerWifi, IoControllerBle, - IoControllerBluetooth, BaseIoController): - """Defines base interfaces for ESP32 microcontrollers""" +class Esp32_Interfaces( + IoControllerSpiPeripheral, + IoControllerI2cTarget, + IoControllerTouchDriver, + IoControllerDac, + IoControllerCan, + IoControllerDvp8, + IoControllerI2s, + IoControllerWifi, + IoControllerBle, + IoControllerBluetooth, + BaseIoController, +): + """Defines base interfaces for ESP32 microcontrollers""" @non_library class Esp32_Ios(Esp32_Interfaces, BaseIoControllerPinmapGenerator): - RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name - - @abstractmethod - def _vddio(self) -> Port[VoltageLink]: - """Returns VDDIO (can be VoltageSink or VoltageSource).""" - ... - - def _vdd_model(self) -> VoltageSink: - return VoltageSink( - voltage_limits=(3.0, 3.6)*Volt, # section 5.2, table 14, most restrictive limits - current_draw=(0.001, 370)*mAmp + self.io_current_draw.upper() # from power off (table 8) to RF working (WRROM datasheet table 9) - ) - - def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: - return DigitalBidir.from_supply( # section 5.2, table 15 - self.gnd, pwr, - voltage_limit_tolerance=(-0.3, 0.3) * Volt, - current_limits=(-28, 40)*mAmp, - input_threshold_factor=(0.25, 0.75), - pullup_capable=True, pulldown_capable=True, - ) - - @override - def _io_pinmap(self) -> PinMapUtil: - pwr = self._vddio() - dio_model = self._dio_model(pwr) - sdio_model = DigitalBidir.from_supply( # section 5.2, table 15, for SDIO power domain pins - self.gnd, pwr, - voltage_limit_tolerance=(-0.3, 0.3) * Volt, - current_limits=(-28, 20)*mAmp, # reduced sourcing capability - input_threshold_factor=(0.25, 0.75), - pullup_capable=True, pulldown_capable=True, - ) - - adc_model = AnalogSink.from_supply( - self.gnd, pwr, - signal_limit_abs=(0.1, 2.45)*Volt, # table 3-4, effective ADC range - # TODO: impedance / leakage - not specified by datasheet - ) - - dac_model = AnalogSource.from_supply(self.gnd, pwr) # TODO: no specs in datasheet?! - - uart_model = UartPort(DigitalBidir.empty()) - spi_model = SpiController(DigitalBidir.empty(), (0, 80) * MHertz) # section 4.1.17 - spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), (0, 80) * MHertz) - i2c_model = I2cController(DigitalBidir.empty()) # section 4.1.11, 100/400kHz and up to 5MHz - i2c_target_model = I2cTarget(DigitalBidir.empty()) - touch_model = TouchDriver() - can_model = CanControllerPort(DigitalBidir.empty()) # aka TWAI - i2s_model = I2sController(DigitalBidir.empty()) - dvp8_model = Dvp8Host(DigitalBidir.empty()) - - return PinMapUtil([ # section 2.2, table 1 - # VDD3P3_RTC - PinResource('SENSOR_VP', {'ADC1_CH0': adc_model}), # also input-only 'GPIO36': dio_model, RTC_GPIO - PinResource('SENSOR_CAPP', {'ADC1_CH1': adc_model}), # also input-only 'GPIO37': dio_model, RTC_GPIO - PinResource('SENSOR_CAPN', {'ADC1_CH2': adc_model}), # also input-only 'GPIO38': dio_model, RTC_GPIO - PinResource('SENSOR_VN', {'ADC1_CH3': adc_model}), # also input-only 'GPIO39': dio_model, RTC_GPIO - - PinResource('VDET_1', {'ADC1_CH6': adc_model}), # also input-only 'GPIO34': dio_model, RTC_GPIO - PinResource('VDET_2', {'ADC1_CH7': adc_model}), # also input-only 'GPIO35': dio_model, RTC_GPIO - PinResource('32K_XP', {'GPIO32': dio_model, 'ADC1_CH4': adc_model, 'TOUCH9': touch_model}), # also RTC_GPIO, 32K_XP - PinResource('32K_XN', {'GPIO33': dio_model, 'ADC1_CH5': adc_model, 'TOUCH8': touch_model}), # also RTC_GPIO, 32K_XN - - PinResource('GPIO25', {'GPIO25': dio_model, 'ADC2_CH8': adc_model, 'DAC_1': dac_model}), # also RTC_GPIO - PinResource('GPIO26', {'GPIO26': dio_model, 'ADC2_CH9': adc_model, 'DAC_2': dac_model}), # also RTC_GPIO - PinResource('GPIO27', {'GPIO27': dio_model, 'ADC2_CH7': adc_model, 'TOUCH7': touch_model}), # also RTC_GPIO - - PinResource('MTMS', {'GPIO14': dio_model, 'ADC2_CH6': adc_model, 'TOUCH6': touch_model}), # also RTC_GPIO - PinResource('MTDI', {'GPIO12': dio_model, 'ADC2_CH5': adc_model, 'TOUCH5': touch_model}), # also RTC_GPIO, noncritical strapping pin - PinResource('MTCK', {'GPIO13': dio_model, 'ADC2_CH4': adc_model, 'TOUCH4': touch_model}), # also RTC_GPIO - PinResource('MTDO', {'GPIO15': dio_model, 'ADC2_CH3': adc_model, 'TOUCH3': touch_model}), # also RTC_GPIO, noncritical strapping pin - - # PinResource('GPIO2', {'GPIO2': self._dio_model, 'ADC2_CH2': adc_model, 'TOUCH2': touch_model}), # also RTC_GPIO, strapping pin - # PinResource('GPIO0', {'GPIO0': self._dio_model, 'ADC2_CH1': adc_model, 'TOUCH1': touch_model}), # also RTC_GPIO, strapping pin - PinResource('GPIO4', {'GPIO4': dio_model, 'ADC2_CH0': adc_model, 'TOUCH0': touch_model}), # also RTC_GPIO - - # VDD_SDIO - PinResource('GPIO16', {'GPIO16': sdio_model}), - PinResource('GPIO17', {'GPIO17': sdio_model}), - PinResource('SD_DATA_2', {'GPIO9': sdio_model}), - PinResource('SD_DATA_3', {'GPIO10': sdio_model}), - PinResource('SD_CMD', {'GPIO11': sdio_model}), - PinResource('SD_CLK', {'GPIO6': sdio_model}), - PinResource('SD_DATA_0', {'GPIO7': sdio_model}), - PinResource('SD_DATA_1', {'GPIO8': sdio_model}), - - # VDD_3P3_CPU - PinResource('GPIO5', {'GPIO5': dio_model}), - PinResource('GPIO18', {'GPIO18': dio_model}), - PinResource('GPIO23', {'GPIO23': dio_model}), - PinResource('GPIO19', {'GPIO19': dio_model}), - PinResource('GPIO22', {'GPIO22': dio_model}), - # PinResource('U0RXD', {'GPIO3': dio_model}), # for programming, technically reallocatable - # PinResource('U0TXD', {'GPIO1': dio_model}), # for programming, technically reallocatable - PinResource('GPIO21', {'GPIO21': dio_model}), - - # section 4.2, table 12: peripheral pin assignments - # note LED and motor PWMs can be assigned to any pin - # PeripheralAnyResource('U0', uart_model), # for programming, technically reallocatable - PeripheralAnyResource('U1', uart_model), - PeripheralAnyResource('U2', uart_model), - - PeripheralAnyResource('I2CEXT0', i2c_model), - PeripheralAnyResource('I2CEXT1', i2c_model), - PeripheralAnyResource('I2CEXT0_T', i2c_target_model), # TODO shared resource w/ I2C controller - PeripheralAnyResource('I2CEXT1_T', i2c_target_model), # TODO shared resource w/ I2C controller - - # PeripheralAnyResource('SPI', spi_model), # for flash, non-allocatable - PeripheralAnyResource('HSPI', spi_model), - PeripheralAnyResource('VSPI', spi_model), - PeripheralAnyResource('HSPI_P', spi_peripheral_model), # TODO shared resource w/ SPI controller - PeripheralAnyResource('VSPI_P', spi_peripheral_model), # TODO shared resource w/ SPI controller - - PeripheralAnyResource('TWAI', can_model), - - PeripheralAnyResource('I2S0', i2s_model), # while CLK is restricted pinning, SCK = BCK here - PeripheralAnyResource('I2S1', i2s_model), - - PeripheralAnyResource('DVP', dvp8_model), # TODO this also eats an I2S port, also available as 16-bit - ]).remap_pins(self.RESOURCE_PIN_REMAP) + RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name + + @abstractmethod + def _vddio(self) -> Port[VoltageLink]: + """Returns VDDIO (can be VoltageSink or VoltageSource).""" + ... + + def _vdd_model(self) -> VoltageSink: + return VoltageSink( + voltage_limits=(3.0, 3.6) * Volt, # section 5.2, table 14, most restrictive limits + current_draw=(0.001, 370) * mAmp + + self.io_current_draw.upper(), # from power off (table 8) to RF working (WRROM datasheet table 9) + ) + + def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: + return DigitalBidir.from_supply( # section 5.2, table 15 + self.gnd, + pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_limits=(-28, 40) * mAmp, + input_threshold_factor=(0.25, 0.75), + pullup_capable=True, + pulldown_capable=True, + ) + + @override + def _io_pinmap(self) -> PinMapUtil: + pwr = self._vddio() + dio_model = self._dio_model(pwr) + sdio_model = DigitalBidir.from_supply( # section 5.2, table 15, for SDIO power domain pins + self.gnd, + pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_limits=(-28, 20) * mAmp, # reduced sourcing capability + input_threshold_factor=(0.25, 0.75), + pullup_capable=True, + pulldown_capable=True, + ) + + adc_model = AnalogSink.from_supply( + self.gnd, + pwr, + signal_limit_abs=(0.1, 2.45) * Volt, # table 3-4, effective ADC range + # TODO: impedance / leakage - not specified by datasheet + ) + + dac_model = AnalogSource.from_supply(self.gnd, pwr) # TODO: no specs in datasheet?! + + uart_model = UartPort(DigitalBidir.empty()) + spi_model = SpiController(DigitalBidir.empty(), (0, 80) * MHertz) # section 4.1.17 + spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), (0, 80) * MHertz) + i2c_model = I2cController(DigitalBidir.empty()) # section 4.1.11, 100/400kHz and up to 5MHz + i2c_target_model = I2cTarget(DigitalBidir.empty()) + touch_model = TouchDriver() + can_model = CanControllerPort(DigitalBidir.empty()) # aka TWAI + i2s_model = I2sController(DigitalBidir.empty()) + dvp8_model = Dvp8Host(DigitalBidir.empty()) + + return PinMapUtil( + [ # section 2.2, table 1 + # VDD3P3_RTC + PinResource("SENSOR_VP", {"ADC1_CH0": adc_model}), # also input-only 'GPIO36': dio_model, RTC_GPIO + PinResource("SENSOR_CAPP", {"ADC1_CH1": adc_model}), # also input-only 'GPIO37': dio_model, RTC_GPIO + PinResource("SENSOR_CAPN", {"ADC1_CH2": adc_model}), # also input-only 'GPIO38': dio_model, RTC_GPIO + PinResource("SENSOR_VN", {"ADC1_CH3": adc_model}), # also input-only 'GPIO39': dio_model, RTC_GPIO + PinResource("VDET_1", {"ADC1_CH6": adc_model}), # also input-only 'GPIO34': dio_model, RTC_GPIO + PinResource("VDET_2", {"ADC1_CH7": adc_model}), # also input-only 'GPIO35': dio_model, RTC_GPIO + PinResource( + "32K_XP", {"GPIO32": dio_model, "ADC1_CH4": adc_model, "TOUCH9": touch_model} + ), # also RTC_GPIO, 32K_XP + PinResource( + "32K_XN", {"GPIO33": dio_model, "ADC1_CH5": adc_model, "TOUCH8": touch_model} + ), # also RTC_GPIO, 32K_XN + PinResource( + "GPIO25", {"GPIO25": dio_model, "ADC2_CH8": adc_model, "DAC_1": dac_model} + ), # also RTC_GPIO + PinResource( + "GPIO26", {"GPIO26": dio_model, "ADC2_CH9": adc_model, "DAC_2": dac_model} + ), # also RTC_GPIO + PinResource( + "GPIO27", {"GPIO27": dio_model, "ADC2_CH7": adc_model, "TOUCH7": touch_model} + ), # also RTC_GPIO + PinResource( + "MTMS", {"GPIO14": dio_model, "ADC2_CH6": adc_model, "TOUCH6": touch_model} + ), # also RTC_GPIO + PinResource( + "MTDI", {"GPIO12": dio_model, "ADC2_CH5": adc_model, "TOUCH5": touch_model} + ), # also RTC_GPIO, noncritical strapping pin + PinResource( + "MTCK", {"GPIO13": dio_model, "ADC2_CH4": adc_model, "TOUCH4": touch_model} + ), # also RTC_GPIO + PinResource( + "MTDO", {"GPIO15": dio_model, "ADC2_CH3": adc_model, "TOUCH3": touch_model} + ), # also RTC_GPIO, noncritical strapping pin + # PinResource('GPIO2', {'GPIO2': self._dio_model, 'ADC2_CH2': adc_model, 'TOUCH2': touch_model}), # also RTC_GPIO, strapping pin + # PinResource('GPIO0', {'GPIO0': self._dio_model, 'ADC2_CH1': adc_model, 'TOUCH1': touch_model}), # also RTC_GPIO, strapping pin + PinResource( + "GPIO4", {"GPIO4": dio_model, "ADC2_CH0": adc_model, "TOUCH0": touch_model} + ), # also RTC_GPIO + # VDD_SDIO + PinResource("GPIO16", {"GPIO16": sdio_model}), + PinResource("GPIO17", {"GPIO17": sdio_model}), + PinResource("SD_DATA_2", {"GPIO9": sdio_model}), + PinResource("SD_DATA_3", {"GPIO10": sdio_model}), + PinResource("SD_CMD", {"GPIO11": sdio_model}), + PinResource("SD_CLK", {"GPIO6": sdio_model}), + PinResource("SD_DATA_0", {"GPIO7": sdio_model}), + PinResource("SD_DATA_1", {"GPIO8": sdio_model}), + # VDD_3P3_CPU + PinResource("GPIO5", {"GPIO5": dio_model}), + PinResource("GPIO18", {"GPIO18": dio_model}), + PinResource("GPIO23", {"GPIO23": dio_model}), + PinResource("GPIO19", {"GPIO19": dio_model}), + PinResource("GPIO22", {"GPIO22": dio_model}), + # PinResource('U0RXD', {'GPIO3': dio_model}), # for programming, technically reallocatable + # PinResource('U0TXD', {'GPIO1': dio_model}), # for programming, technically reallocatable + PinResource("GPIO21", {"GPIO21": dio_model}), + # section 4.2, table 12: peripheral pin assignments + # note LED and motor PWMs can be assigned to any pin + # PeripheralAnyResource('U0', uart_model), # for programming, technically reallocatable + PeripheralAnyResource("U1", uart_model), + PeripheralAnyResource("U2", uart_model), + PeripheralAnyResource("I2CEXT0", i2c_model), + PeripheralAnyResource("I2CEXT1", i2c_model), + PeripheralAnyResource("I2CEXT0_T", i2c_target_model), # TODO shared resource w/ I2C controller + PeripheralAnyResource("I2CEXT1_T", i2c_target_model), # TODO shared resource w/ I2C controller + # PeripheralAnyResource('SPI', spi_model), # for flash, non-allocatable + PeripheralAnyResource("HSPI", spi_model), + PeripheralAnyResource("VSPI", spi_model), + PeripheralAnyResource("HSPI_P", spi_peripheral_model), # TODO shared resource w/ SPI controller + PeripheralAnyResource("VSPI_P", spi_peripheral_model), # TODO shared resource w/ SPI controller + PeripheralAnyResource("TWAI", can_model), + PeripheralAnyResource("I2S0", i2s_model), # while CLK is restricted pinning, SCK = BCK here + PeripheralAnyResource("I2S1", i2s_model), + PeripheralAnyResource("DVP", dvp8_model), # TODO this also eats an I2S port, also available as 16-bit + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) @abstract_block class Esp32_Base(Esp32_Ios, GeneratorBlock): - """Base class for ESP32 series microcontrollers with WiFi and Bluetooth (classic and LE) - - Chip datasheet: https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf - """ - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.pwr = self.Port(self._vdd_model(), [Power]) - self.gnd = self.Port(Ground(), [Common]) - - dio_model = self._dio_model(self.pwr) - self.chip_pu = self.Port(dio_model) # power control, must NOT be left floating, table 1 - # section 2.4, table 5: strapping IOs that need a fixed value to boot, TODO currently not allocatable post-boot - self.io0 = self.Port(dio_model, optional=True) # default pullup (SPI boot), set low to download boot - self.io2 = self.Port(dio_model, optional=True) # default pulldown (enable download boot), ignored during SPI boot - - # similarly, the programming UART is fixed and allocated separately - self.uart0 = self.Port(UartPort(dio_model), optional=True) - - @override - def _vddio(self) -> Port[VoltageLink]: - return self.pwr - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ - 'Vdd': self.pwr, - 'Vss': self.gnd, - 'CHIP_PU': self.chip_pu, - 'GPIO0': self.io0, - 'GPIO2': self.io2, - # 'MTDO': ..., # disconnected, internally pulled up for strapping - U0TXD active - # 'MTDI': ..., # disconnected, internally pulled down for strapping - 3.3v LDO - 'U0RXD': self.uart0.rx, - 'U0TXD': self.uart0.tx, - }).remap(self.SYSTEM_PIN_REMAP) + """Base class for ESP32 series microcontrollers with WiFi and Bluetooth (classic and LE) + + Chip datasheet: https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf + """ + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.pwr = self.Port(self._vdd_model(), [Power]) + self.gnd = self.Port(Ground(), [Common]) + + dio_model = self._dio_model(self.pwr) + self.chip_pu = self.Port(dio_model) # power control, must NOT be left floating, table 1 + # section 2.4, table 5: strapping IOs that need a fixed value to boot, TODO currently not allocatable post-boot + self.io0 = self.Port(dio_model, optional=True) # default pullup (SPI boot), set low to download boot + self.io2 = self.Port( + dio_model, optional=True + ) # default pulldown (enable download boot), ignored during SPI boot + + # similarly, the programming UART is fixed and allocated separately + self.uart0 = self.Port(UartPort(dio_model), optional=True) + + @override + def _vddio(self) -> Port[VoltageLink]: + return self.pwr + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return VariantPinRemapper( + { + "Vdd": self.pwr, + "Vss": self.gnd, + "CHIP_PU": self.chip_pu, + "GPIO0": self.io0, + "GPIO2": self.io2, + # 'MTDO': ..., # disconnected, internally pulled up for strapping - U0TXD active + # 'MTDI': ..., # disconnected, internally pulled down for strapping - 3.3v LDO + "U0RXD": self.uart0.rx, + "U0TXD": self.uart0.tx, + } + ).remap(self.SYSTEM_PIN_REMAP) class Esp32_Wroom_32_Device(Esp32_Base, InternalSubcircuit, FootprintBlock, JlcPart): - """ESP32-WROOM-32 module - - Module datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32e_esp32-wroom-32ue_datasheet_en.pdf - """ - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'Vdd': '2', - 'Vss': ['1', '15', '38', '39'], # 39 is EP - 'CHIP_PU': '3', # aka EN - - 'GPIO0': '25', - 'GPIO2': '24', - - 'U0RXD': '34', - 'U0TXD': '35', - } - - RESOURCE_PIN_REMAP = { - 'SENSOR_VP': '4', - 'SENSOR_VN': '5', - 'VDET_1': '6', - 'VDET_2': '7', - '32K_XP': '8', - '32K_XN': '9', - 'GPIO25': '10', - 'GPIO26': '11', - 'GPIO27': '12', - 'MTMS': '13', - 'MTDI': '14', # aka GPIO12 - - 'MTCK': '16', - # pins 17-22 NC - - 'MTDO': '23', # aka GPIO15 - 'GPIO4': '26', - 'GPIO16': '27', # for QSPI PSRAM variants this cannot be used - 'GPIO17': '28', - 'GPIO5': '29', - 'GPIO18': '30', - 'GPIO19': '31', - # pin 32 NC - 'GPIO21': '33', - 'GPIO22': '36', - 'GPIO23': '37', - } - - @override - def generate(self) -> None: - super().generate() - - self.assign(self.lcsc_part, 'C701342') - self.assign(self.actual_basic_part, False) - self.footprint( - 'U', 'RF_Module:ESP32-WROOM-32', - self._make_pinning(), - mfr='Espressif Systems', part='ESP32-WROOM-32', - datasheet='https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32e_esp32-wroom-32ue_datasheet_en.pdf', - ) - - -class Esp32_Wroom_32(Microcontroller, Radiofrequency, HasEspProgramming, Resettable, Esp32_Interfaces, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): - """Wrapper around Esp32c3_Wroom02 with external capacitors and UART programming header. - NOT COMPATIBLE WITH QSPI PSRAM VARIANTS - for those, GPIO16 needs to be pulled up. - """ - def __init__(self) -> None: - super().__init__() - self.ic: Esp32_Wroom_32_Device - self.generator_param(self.reset.is_connected()) - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.ic = imp.Block(Esp32_Wroom_32_Device(pin_assigns=ArrayStringExpr())) - self.connect(self.program_uart_node, self.ic.uart0) - self.connect(self.program_en_node, self.ic.chip_pu) - self.connect(self.program_boot_node, self.ic.io0) - # strapping pins are by default pulled to SPI boot, and can be reconfigured to download boot - - self.vcc_cap0 = imp.Block(DecouplingCapacitor(22 * uFarad(tol=0.2))) # C1 - self.vcc_cap1 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) # C2 - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.chip_pu) - else: - self.en_pull = self.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10*mSecond(tol=0.2))).connected( - gnd=self.gnd, pwr=self.pwr, io=self.ic.chip_pu) - - -class Freenove_Esp32_Wrover(IoControllerUsbOut, IoControllerPowerOut, Esp32_Ios, IoController, GeneratorBlock, - FootprintBlock): - """ESP32-WROVER-DEV breakout with camera. - - Module datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-wrover-e_esp32-wrover-ie_datasheet_en.pdf - Board used: https://amazon.com/ESP32-WROVER-Contained-Compatible-Bluetooth-Tutorials/dp/B09BC1N9LL - Board internal schematic: https://github.com/Freenove/Freenove_ESP32_WROVER_Board/blob/f710fd6976e76ab76c29c2ee3042cd7bac22c3d6/Datasheet/ESP32_Schematic.pdf - - Top left is pin 1, going down the left side then up the right side. - Up is defined from the text orientation (antenna is on top). - """ - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'Vdd': '1', # 3v3, output of internal AMS1117-3.3V LDO - 'Vusb': ['19', '20'], # Vcc 5vUSB, input to internal LDO - 'Vss': ['14', '21', '29', '30', '34', '40'], - - 'GPIO2': '26', # fixed strapping pin, drives LED on PCB - } - RESOURCE_PIN_REMAP = { - # 'SENSOR_VP': '3', # camera CSI_Y6 - # 'SENSOR_VN': '4', # camera CSI_Y7 - # 'VDET_1': '5', # input only GPIO34, CSI_Y8 - # 'VDET_2': '6', # input only GPIO35, CSI_Y9 - '32K_XP': '7', # GPIO32 - '32K_XN': '8', # GPIO33 - # 'GPIO25': '9', # camera CSI_VYSNC - # 'GPIO26': '10', # camera I2C_SDA - # 'GPIO27': '11', # camera I2C_SCL - 'MTMS': '12', # GPIO14 - 'MTDI': '13', # GPIO12 - - 'MTCK': '15', # GPIO13 - # 'SD_DATA_2': '16', # FLASH_D2, SD2 - # 'SD_DATA_3': '17', # DLASH_D3, SD3 - # 'SD_CMD': '18', # FLASH_CMD, CMD - - # 'SD_CLK': '22', # FLASH_CLK, SD0 - # 'SD_DATA_0': '23', # FLASH_D0, SD1 - # 'SD_DATA_1': '24', # FLASH_D1, SD1 - 'MTDO': '25', # GPIO15 - - # 'GPIO4': '28', # camera CSI_Y2 - - # 'GPIO5': '31', # camera CSI_Y3 - # 'GPIO18': '32', # camera CSI_Y4 - # 'GPIO19': '33', # camera CSI_Y5 - - # 'GPIO21': '35', # camera XCLK - - # 'GPIO22': '38', # camera CSI_PCLK - # 'GPIO23': '39', # camera CSI_HREF - } - - @override - def _vddio(self) -> Port[VoltageLink]: - if self.get(self.pwr.is_connected()): # board sinks power - return self.pwr - else: - return self.pwr_out - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - if self.get(self.pwr.is_connected()): # board sinks power - self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") - self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") - return VariantPinRemapper({ - 'Vdd': self.pwr, - 'Vss': self.gnd, - 'GPIO2': self.io2, - }).remap(self.SYSTEM_PIN_REMAP) - else: # board sources power (default) - return VariantPinRemapper({ - 'Vdd': self.pwr_out, - 'Vss': self.gnd, - 'Vusb': self.vusb_out, - 'GPIO2': self.io2, - }).remap(self.SYSTEM_PIN_REMAP) - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.gnd.init_from(Ground()) - self.pwr.init_from(self._vdd_model()) - - self.vusb_out.init_from(VoltageSource( - voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - self.pwr_out.init_from(VoltageSource( - voltage_out=3.3*Volt(tol=0.05), # tolerance is a guess - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - - self.io2 = self.Port(DigitalBidir.empty(), optional=True) # default pulldown (enable download boot), ignored during SPI boot - - self.generator_param(self.pwr.is_connected()) - - @override - def generate(self) -> None: - super().generate() - - gnd, pwr = self._gnd_vddio() - self.io2.init_from(self._dio_model(pwr)) # TODO remove this hack - - self.footprint( - 'U', 'edg:Freenove_ESP32-WROVER', - self._make_pinning(), - mfr='', part='Freenove ESP32-WROVER', - ) + """ESP32-WROOM-32 module + + Module datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32e_esp32-wroom-32ue_datasheet_en.pdf + """ + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "Vdd": "2", + "Vss": ["1", "15", "38", "39"], # 39 is EP + "CHIP_PU": "3", # aka EN + "GPIO0": "25", + "GPIO2": "24", + "U0RXD": "34", + "U0TXD": "35", + } + + RESOURCE_PIN_REMAP = { + "SENSOR_VP": "4", + "SENSOR_VN": "5", + "VDET_1": "6", + "VDET_2": "7", + "32K_XP": "8", + "32K_XN": "9", + "GPIO25": "10", + "GPIO26": "11", + "GPIO27": "12", + "MTMS": "13", + "MTDI": "14", # aka GPIO12 + "MTCK": "16", + # pins 17-22 NC + "MTDO": "23", # aka GPIO15 + "GPIO4": "26", + "GPIO16": "27", # for QSPI PSRAM variants this cannot be used + "GPIO17": "28", + "GPIO5": "29", + "GPIO18": "30", + "GPIO19": "31", + # pin 32 NC + "GPIO21": "33", + "GPIO22": "36", + "GPIO23": "37", + } + + @override + def generate(self) -> None: + super().generate() + + self.assign(self.lcsc_part, "C701342") + self.assign(self.actual_basic_part, False) + self.footprint( + "U", + "RF_Module:ESP32-WROOM-32", + self._make_pinning(), + mfr="Espressif Systems", + part="ESP32-WROOM-32", + datasheet="https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32e_esp32-wroom-32ue_datasheet_en.pdf", + ) + + +class Esp32_Wroom_32( + Microcontroller, + Radiofrequency, + HasEspProgramming, + Resettable, + Esp32_Interfaces, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): + """Wrapper around Esp32c3_Wroom02 with external capacitors and UART programming header. + NOT COMPATIBLE WITH QSPI PSRAM VARIANTS - for those, GPIO16 needs to be pulled up. + """ + + def __init__(self) -> None: + super().__init__() + self.ic: Esp32_Wroom_32_Device + self.generator_param(self.reset.is_connected()) + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.ic = imp.Block(Esp32_Wroom_32_Device(pin_assigns=ArrayStringExpr())) + self.connect(self.program_uart_node, self.ic.uart0) + self.connect(self.program_en_node, self.ic.chip_pu) + self.connect(self.program_boot_node, self.ic.io0) + # strapping pins are by default pulled to SPI boot, and can be reconfigured to download boot + + self.vcc_cap0 = imp.Block(DecouplingCapacitor(22 * uFarad(tol=0.2))) # C1 + self.vcc_cap1 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) # C2 + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.chip_pu) + else: + self.en_pull = self.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10 * mSecond(tol=0.2))).connected( + gnd=self.gnd, pwr=self.pwr, io=self.ic.chip_pu + ) + + +class Freenove_Esp32_Wrover( + IoControllerUsbOut, IoControllerPowerOut, Esp32_Ios, IoController, GeneratorBlock, FootprintBlock +): + """ESP32-WROVER-DEV breakout with camera. + + Module datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-wrover-e_esp32-wrover-ie_datasheet_en.pdf + Board used: https://amazon.com/ESP32-WROVER-Contained-Compatible-Bluetooth-Tutorials/dp/B09BC1N9LL + Board internal schematic: https://github.com/Freenove/Freenove_ESP32_WROVER_Board/blob/f710fd6976e76ab76c29c2ee3042cd7bac22c3d6/Datasheet/ESP32_Schematic.pdf + + Top left is pin 1, going down the left side then up the right side. + Up is defined from the text orientation (antenna is on top). + """ + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "Vdd": "1", # 3v3, output of internal AMS1117-3.3V LDO + "Vusb": ["19", "20"], # Vcc 5vUSB, input to internal LDO + "Vss": ["14", "21", "29", "30", "34", "40"], + "GPIO2": "26", # fixed strapping pin, drives LED on PCB + } + RESOURCE_PIN_REMAP = { + # 'SENSOR_VP': '3', # camera CSI_Y6 + # 'SENSOR_VN': '4', # camera CSI_Y7 + # 'VDET_1': '5', # input only GPIO34, CSI_Y8 + # 'VDET_2': '6', # input only GPIO35, CSI_Y9 + "32K_XP": "7", # GPIO32 + "32K_XN": "8", # GPIO33 + # 'GPIO25': '9', # camera CSI_VYSNC + # 'GPIO26': '10', # camera I2C_SDA + # 'GPIO27': '11', # camera I2C_SCL + "MTMS": "12", # GPIO14 + "MTDI": "13", # GPIO12 + "MTCK": "15", # GPIO13 + # 'SD_DATA_2': '16', # FLASH_D2, SD2 + # 'SD_DATA_3': '17', # DLASH_D3, SD3 + # 'SD_CMD': '18', # FLASH_CMD, CMD + # 'SD_CLK': '22', # FLASH_CLK, SD0 + # 'SD_DATA_0': '23', # FLASH_D0, SD1 + # 'SD_DATA_1': '24', # FLASH_D1, SD1 + "MTDO": "25", # GPIO15 + # 'GPIO4': '28', # camera CSI_Y2 + # 'GPIO5': '31', # camera CSI_Y3 + # 'GPIO18': '32', # camera CSI_Y4 + # 'GPIO19': '33', # camera CSI_Y5 + # 'GPIO21': '35', # camera XCLK + # 'GPIO22': '38', # camera CSI_PCLK + # 'GPIO23': '39', # camera CSI_HREF + } + + @override + def _vddio(self) -> Port[VoltageLink]: + if self.get(self.pwr.is_connected()): # board sinks power + return self.pwr + else: + return self.pwr_out + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + if self.get(self.pwr.is_connected()): # board sinks power + self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") + self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") + return VariantPinRemapper( + { + "Vdd": self.pwr, + "Vss": self.gnd, + "GPIO2": self.io2, + } + ).remap(self.SYSTEM_PIN_REMAP) + else: # board sources power (default) + return VariantPinRemapper( + { + "Vdd": self.pwr_out, + "Vss": self.gnd, + "Vusb": self.vusb_out, + "GPIO2": self.io2, + } + ).remap(self.SYSTEM_PIN_REMAP) + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.gnd.init_from(Ground()) + self.pwr.init_from(self._vdd_model()) + + self.vusb_out.init_from( + VoltageSource(voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, current_limits=UsbConnector.USB2_CURRENT_LIMITS) + ) + self.pwr_out.init_from( + VoltageSource( + voltage_out=3.3 * Volt(tol=0.05), # tolerance is a guess + current_limits=UsbConnector.USB2_CURRENT_LIMITS, + ) + ) + + self.io2 = self.Port( + DigitalBidir.empty(), optional=True + ) # default pulldown (enable download boot), ignored during SPI boot + + self.generator_param(self.pwr.is_connected()) + + @override + def generate(self) -> None: + super().generate() + + gnd, pwr = self._gnd_vddio() + self.io2.init_from(self._dio_model(pwr)) # TODO remove this hack + + self.footprint( + "U", + "edg:Freenove_ESP32-WROVER", + self._make_pinning(), + mfr="", + part="Freenove ESP32-WROVER", + ) diff --git a/edg/parts/Microcontroller_Esp32c3.py b/edg/parts/Microcontroller_Esp32c3.py index fb486f1a4..8d25fc36b 100644 --- a/edg/parts/Microcontroller_Esp32c3.py +++ b/edg/parts/Microcontroller_Esp32c3.py @@ -9,505 +9,578 @@ @non_library -class Esp32c3_Interfaces(IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerCan, IoControllerI2s, - IoControllerWifi, IoControllerBle, BaseIoController): - """Defines base interfaces for ESP32C3 microcontrollers""" +class Esp32c3_Interfaces( + IoControllerSpiPeripheral, + IoControllerI2cTarget, + IoControllerCan, + IoControllerI2s, + IoControllerWifi, + IoControllerBle, + BaseIoController, +): + """Defines base interfaces for ESP32C3 microcontrollers""" @non_library class Esp32c3_Ios(Esp32c3_Interfaces, BaseIoControllerPinmapGenerator): - """IOs definitions independent of infrastructural (e.g. power) pins.""" - RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name - - @abstractmethod - def _vddio(self) -> Port[VoltageLink]: - """Returns VDDIO (can be VoltageSink or VoltageSource).""" - ... - - def _vdd_model(self) -> VoltageSink: - return VoltageSink( - voltage_limits=(3.0, 3.6)*Volt, # section 4.2 - current_draw=(0.001, 335)*mAmp + self.io_current_draw.upper() # section 4.6, from power off to RF active - ) - - def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: - return DigitalBidir.from_supply( # table 4.4 - self.gnd, pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - current_limits=(-28, 40)*mAmp, - input_threshold_factor=(0.25, 0.75), - pullup_capable=True, pulldown_capable=True, - ) - - @override - def _io_pinmap(self) -> PinMapUtil: - pwr = self._vddio() - dio_model = self._dio_model(pwr) - - adc_model = AnalogSink.from_supply( - self.gnd, pwr, - signal_limit_abs=(0, 2.5)*Volt, # table 15, effective ADC range - # TODO: impedance / leakage - not specified by datasheet - ) - - uart_model = UartPort(DigitalBidir.empty()) - spi_model = SpiController(DigitalBidir.empty(), (0, 60) * MHertz) # section 3.4.2, max block in GP controller mode - spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), (0, 60) * MHertz) - i2c_model = I2cController(DigitalBidir.empty()) # section 3.4.4, supporting 100/400 and up to 800 kbit/s - i2c_target_model = I2cTarget(DigitalBidir.empty()) - - return PinMapUtil([ # section 2.2 - PinResource('GPIO0', {'GPIO0': dio_model, 'ADC1_CH0': adc_model}), # also XTAL_32K_P - PinResource('GPIO1', {'GPIO1': dio_model, 'ADC1_CH1': adc_model}), # also XTAL_32K_N - # PinResource('GPIO2', {'GPIO2': dio_model, 'ADC1_CH2': adc_model}), # boot pin, non-allocatable - PinResource('GPIO3', {'GPIO3': dio_model, 'ADC1_CH3': adc_model}), - PinResource('MTMS', {'GPIO4': dio_model, 'ADC1_CH4': adc_model}), - PinResource('MTDI', {'GPIO5': dio_model}), # also ADC2_CH0, but unusable with WiFi - PinResource('MTCK', {'GPIO6': dio_model}), - PinResource('MTDO', {'GPIO7': dio_model}), - # PinResource('GPIO8', {'GPIO8': dio_model}), # boot pin, non-allocatable - # PinResource('GPIO9', {'GPIO9': dio_model}), # boot pin, non-allocatable - PinResource('GPIO10', {'GPIO10': dio_model}), - PinResource('VDD_SPI', {'GPIO11': dio_model}), - # SPI pins skipped - internal to the modules supported so far - PinResource('GPIO18', {'GPIO18': dio_model}), - PinResource('GPIO19', {'GPIO19': dio_model}), - # PinResource('GPIO20', {'GPIO20': dio_model}), # boot pin, non-allocatable - # PinResource('GPIO21', {'GPIO21': dio_model}), # boot pin, non-allocatable - - # peripherals in section 3.11 - # PeripheralFixedResource('U0', uart_model, { # programming pin, non-allocatable - # 'txd': ['GPIO21'], 'rxd': ['GPIO20'] - # }), - PeripheralAnyResource('U1', uart_model), - PeripheralAnyResource('I2C', i2c_model), - PeripheralAnyResource('I2C_T', i2c_target_model), # TODO shared resource w/ I2C controller - PeripheralAnyResource('SPI2', spi_model), - PeripheralAnyResource('SPI2_P', spi_peripheral_model), # TODO shared resource w/ SPI controller - PeripheralAnyResource('I2S', I2sController.empty()), - PeripheralAnyResource('TWAI', CanControllerPort.empty()), - ]).remap_pins(self.RESOURCE_PIN_REMAP) + """IOs definitions independent of infrastructural (e.g. power) pins.""" + + RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name + + @abstractmethod + def _vddio(self) -> Port[VoltageLink]: + """Returns VDDIO (can be VoltageSink or VoltageSource).""" + ... + + def _vdd_model(self) -> VoltageSink: + return VoltageSink( + voltage_limits=(3.0, 3.6) * Volt, # section 4.2 + current_draw=(0.001, 335) * mAmp + self.io_current_draw.upper(), # section 4.6, from power off to RF active + ) + + def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: + return DigitalBidir.from_supply( # table 4.4 + self.gnd, + pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_limits=(-28, 40) * mAmp, + input_threshold_factor=(0.25, 0.75), + pullup_capable=True, + pulldown_capable=True, + ) + + @override + def _io_pinmap(self) -> PinMapUtil: + pwr = self._vddio() + dio_model = self._dio_model(pwr) + + adc_model = AnalogSink.from_supply( + self.gnd, + pwr, + signal_limit_abs=(0, 2.5) * Volt, # table 15, effective ADC range + # TODO: impedance / leakage - not specified by datasheet + ) + + uart_model = UartPort(DigitalBidir.empty()) + spi_model = SpiController( + DigitalBidir.empty(), (0, 60) * MHertz + ) # section 3.4.2, max block in GP controller mode + spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), (0, 60) * MHertz) + i2c_model = I2cController(DigitalBidir.empty()) # section 3.4.4, supporting 100/400 and up to 800 kbit/s + i2c_target_model = I2cTarget(DigitalBidir.empty()) + + return PinMapUtil( + [ # section 2.2 + PinResource("GPIO0", {"GPIO0": dio_model, "ADC1_CH0": adc_model}), # also XTAL_32K_P + PinResource("GPIO1", {"GPIO1": dio_model, "ADC1_CH1": adc_model}), # also XTAL_32K_N + # PinResource('GPIO2', {'GPIO2': dio_model, 'ADC1_CH2': adc_model}), # boot pin, non-allocatable + PinResource("GPIO3", {"GPIO3": dio_model, "ADC1_CH3": adc_model}), + PinResource("MTMS", {"GPIO4": dio_model, "ADC1_CH4": adc_model}), + PinResource("MTDI", {"GPIO5": dio_model}), # also ADC2_CH0, but unusable with WiFi + PinResource("MTCK", {"GPIO6": dio_model}), + PinResource("MTDO", {"GPIO7": dio_model}), + # PinResource('GPIO8', {'GPIO8': dio_model}), # boot pin, non-allocatable + # PinResource('GPIO9', {'GPIO9': dio_model}), # boot pin, non-allocatable + PinResource("GPIO10", {"GPIO10": dio_model}), + PinResource("VDD_SPI", {"GPIO11": dio_model}), + # SPI pins skipped - internal to the modules supported so far + PinResource("GPIO18", {"GPIO18": dio_model}), + PinResource("GPIO19", {"GPIO19": dio_model}), + # PinResource('GPIO20', {'GPIO20': dio_model}), # boot pin, non-allocatable + # PinResource('GPIO21', {'GPIO21': dio_model}), # boot pin, non-allocatable + # peripherals in section 3.11 + # PeripheralFixedResource('U0', uart_model, { # programming pin, non-allocatable + # 'txd': ['GPIO21'], 'rxd': ['GPIO20'] + # }), + PeripheralAnyResource("U1", uart_model), + PeripheralAnyResource("I2C", i2c_model), + PeripheralAnyResource("I2C_T", i2c_target_model), # TODO shared resource w/ I2C controller + PeripheralAnyResource("SPI2", spi_model), + PeripheralAnyResource("SPI2_P", spi_peripheral_model), # TODO shared resource w/ SPI controller + PeripheralAnyResource("I2S", I2sController.empty()), + PeripheralAnyResource("TWAI", CanControllerPort.empty()), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) @abstract_block class Esp32c3_Base(Esp32c3_Ios, BaseIoControllerPinmapGenerator): - """Base class for ESP32-C3 series devices, with RISC-V core, 2.4GHz WiF,i, BLE5. - PlatformIO: use board ID esp32-c3-devkitm-1 - - Chip datasheet: https://espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf - """ - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) - - @override - def _vddio(self) -> Port[VoltageLink]: - return self.pwr - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return { - 'Vdd': self.pwr, - 'Vss': self.gnd, - 'EN': self.en, - 'GPIO2': self.io2, - 'GPIO8': self.io8, - 'GPIO9': self.io9, - 'TXD': self.uart0.tx, - 'RXD': self.uart0.rx, - } + """Base class for ESP32-C3 series devices, with RISC-V core, 2.4GHz WiF,i, BLE5. + PlatformIO: use board ID esp32-c3-devkitm-1 + + Chip datasheet: https://espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf + """ + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) + @override + def _vddio(self) -> Port[VoltageLink]: + return self.pwr - self.pwr = self.Port(self._vdd_model(), [Power]) - self.gnd = self.Port(Ground(), [Common]) + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return { + "Vdd": self.pwr, + "Vss": self.gnd, + "EN": self.en, + "GPIO2": self.io2, + "GPIO8": self.io8, + "GPIO9": self.io9, + "TXD": self.uart0.tx, + "RXD": self.uart0.rx, + } - # section 2.4: strapping IOs that need a fixed value to boot, and currently can't be allocated as GPIO - dio_model = self._dio_model(self.pwr) - self.en = self.Port(dio_model) # needs external pullup - self.io2 = self.Port(dio_model) # needs external pullup; affects IO glitching on boot - self.io8 = self.Port(dio_model) # needs external pullup, required for download boot - self.io9 = self.Port(dio_model, optional=True) # internally pulled up for SPI boot, connect to GND for download + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) - # similarly, the programming UART is fixed and allocated separately - self.uart0 = self.Port(UartPort(dio_model), optional=True) + self.pwr = self.Port(self._vdd_model(), [Power]) + self.gnd = self.Port(Ground(), [Common]) + + # section 2.4: strapping IOs that need a fixed value to boot, and currently can't be allocated as GPIO + dio_model = self._dio_model(self.pwr) + self.en = self.Port(dio_model) # needs external pullup + self.io2 = self.Port(dio_model) # needs external pullup; affects IO glitching on boot + self.io8 = self.Port(dio_model) # needs external pullup, required for download boot + self.io9 = self.Port(dio_model, optional=True) # internally pulled up for SPI boot, connect to GND for download + + # similarly, the programming UART is fixed and allocated separately + self.uart0 = self.Port(UartPort(dio_model), optional=True) class Esp32c3_Wroom02_Device(Esp32c3_Base, InternalSubcircuit, FootprintBlock, JlcPart): - """ESP32C module - - Module datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-c3-wroom-02_datasheet_en.pdf - """ - RESOURCE_PIN_REMAP = { - 'MTMS': '3', # GPIO4 - 'MTDI': '4', # GPIO5 - 'MTCK': '5', # GPIO6 - 'MTDO': '6', # GPIO7 - 'GPIO10': '10', - 'GPIO18': '13', - 'GPIO19': '14', - 'GPIO3': '15', - 'GPIO1': '17', - 'GPIO0': '18', - } - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper(super()._system_pinmap()).remap({ - 'Vdd': '1', - 'Vss': ['9', '19'], # 19 is EP - 'EN': '2', - 'GPIO2': '16', - 'GPIO8': '7', - 'GPIO9': '8', - 'RXD': '11', # RXD, GPIO20 - 'TXD': '12', # TXD, GPIO21 - }) - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', 'RF_Module:ESP-WROOM-02', - self._make_pinning(), - mfr='Espressif Systems', part='ESP32-C3-WROOM-02', - datasheet='https://www.espressif.com/sites/default/files/documentation/esp32-c3-wroom-02_datasheet_en.pdf', - ) - self.assign(self.lcsc_part, 'C2934560') - self.assign(self.actual_basic_part, False) - - -class Esp32c3_Wroom02(Microcontroller, Radiofrequency, HasEspProgramming, Resettable, Esp32c3_Interfaces, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): - """Wrapper around Esp32c3_Wroom02 with external capacitors and UART programming header.""" - def __init__(self) -> None: - super().__init__() - self.ic: Esp32c3_Wroom02_Device - self.generator_param(self.reset.is_connected()) - - self.io2_ext_connected: bool = False - self.io8_ext_connected: bool = False - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.ic = imp.Block(Esp32c3_Wroom02_Device(pin_assigns=ArrayStringExpr())) - self.connect(self.program_uart_node, self.ic.uart0) - self.connect(self.program_en_node, self.ic.en) - self.connect(self.program_boot_node, self.ic.io9) - - self.vcc_cap0 = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) # C1 - self.vcc_cap1 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) # C2 - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.en) - else: - self.en_pull = self.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10*mSecond(tol=0.2))).connected( - gnd=self.gnd, pwr=self.pwr, io=self.ic.en) - - # Note strapping pins (section 3.3) IO2, 8, 9; IO9 is internally pulled up - # IO9 (internally pulled up) is 1 for SPI boot and 0 for download boot - # IO2 must be 1 for both SPI and download boot, while IO8 must be 1 for download boot - if not self.io8_ext_connected: - self.connect(self.ic.io8, self.pwr.as_digital_source()) - self.io8_ext_connected = True # set to ensure this runs after external connections - if not self.io2_ext_connected: - self.connect(self.ic.io2, self.pwr.as_digital_source()) - self.io2_ext_connected = True # set to ensure this runs after external connections - - ExportType = TypeVar('ExportType', bound=Port) - @override - def _make_export_vector(self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, - assign: Optional[str]) -> Optional[str]: - """Add support for _GPIO2/8/9_STRAP and remap them to io2/8/9.""" - if isinstance(self_io, DigitalBidir): - if assign == f'{name}=_GPIO2_STRAP_EXT_PU': # assume external pullup - self.connect(self_io, self.ic.io2) - assert not self.io2_ext_connected # assert not yet hard tied - self.io2_ext_connected = True - return None - elif assign == f'{name}=_GPIO8_STRAP_EXT_PU': # assume external pullup - self.connect(self_io, self.ic.io8) - assert not self.io8_ext_connected # assert not yet hard tied - self.io8_ext_connected = True - return None - elif assign == f'{name}=_GPIO9_STRAP': - self.connect(self_io, self.ic.io9) - return None - return super()._make_export_vector(self_io, inner_vector, name, assign) + """ESP32C module + + Module datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-c3-wroom-02_datasheet_en.pdf + """ + + RESOURCE_PIN_REMAP = { + "MTMS": "3", # GPIO4 + "MTDI": "4", # GPIO5 + "MTCK": "5", # GPIO6 + "MTDO": "6", # GPIO7 + "GPIO10": "10", + "GPIO18": "13", + "GPIO19": "14", + "GPIO3": "15", + "GPIO1": "17", + "GPIO0": "18", + } + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return VariantPinRemapper(super()._system_pinmap()).remap( + { + "Vdd": "1", + "Vss": ["9", "19"], # 19 is EP + "EN": "2", + "GPIO2": "16", + "GPIO8": "7", + "GPIO9": "8", + "RXD": "11", # RXD, GPIO20 + "TXD": "12", # TXD, GPIO21 + } + ) + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + "RF_Module:ESP-WROOM-02", + self._make_pinning(), + mfr="Espressif Systems", + part="ESP32-C3-WROOM-02", + datasheet="https://www.espressif.com/sites/default/files/documentation/esp32-c3-wroom-02_datasheet_en.pdf", + ) + self.assign(self.lcsc_part, "C2934560") + self.assign(self.actual_basic_part, False) + + +class Esp32c3_Wroom02( + Microcontroller, + Radiofrequency, + HasEspProgramming, + Resettable, + Esp32c3_Interfaces, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): + """Wrapper around Esp32c3_Wroom02 with external capacitors and UART programming header.""" + + def __init__(self) -> None: + super().__init__() + self.ic: Esp32c3_Wroom02_Device + self.generator_param(self.reset.is_connected()) + + self.io2_ext_connected: bool = False + self.io8_ext_connected: bool = False + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.ic = imp.Block(Esp32c3_Wroom02_Device(pin_assigns=ArrayStringExpr())) + self.connect(self.program_uart_node, self.ic.uart0) + self.connect(self.program_en_node, self.ic.en) + self.connect(self.program_boot_node, self.ic.io9) + + self.vcc_cap0 = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) # C1 + self.vcc_cap1 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) # C2 + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.en) + else: + self.en_pull = self.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10 * mSecond(tol=0.2))).connected( + gnd=self.gnd, pwr=self.pwr, io=self.ic.en + ) + + # Note strapping pins (section 3.3) IO2, 8, 9; IO9 is internally pulled up + # IO9 (internally pulled up) is 1 for SPI boot and 0 for download boot + # IO2 must be 1 for both SPI and download boot, while IO8 must be 1 for download boot + if not self.io8_ext_connected: + self.connect(self.ic.io8, self.pwr.as_digital_source()) + self.io8_ext_connected = True # set to ensure this runs after external connections + if not self.io2_ext_connected: + self.connect(self.ic.io2, self.pwr.as_digital_source()) + self.io2_ext_connected = True # set to ensure this runs after external connections + + ExportType = TypeVar("ExportType", bound=Port) + + @override + def _make_export_vector( + self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, assign: Optional[str] + ) -> Optional[str]: + """Add support for _GPIO2/8/9_STRAP and remap them to io2/8/9.""" + if isinstance(self_io, DigitalBidir): + if assign == f"{name}=_GPIO2_STRAP_EXT_PU": # assume external pullup + self.connect(self_io, self.ic.io2) + assert not self.io2_ext_connected # assert not yet hard tied + self.io2_ext_connected = True + return None + elif assign == f"{name}=_GPIO8_STRAP_EXT_PU": # assume external pullup + self.connect(self_io, self.ic.io8) + assert not self.io8_ext_connected # assert not yet hard tied + self.io8_ext_connected = True + return None + elif assign == f"{name}=_GPIO9_STRAP": + self.connect(self_io, self.ic.io9) + return None + return super()._make_export_vector(self_io, inner_vector, name, assign) class Esp32c3_Device(Esp32c3_Base, InternalSubcircuit, FootprintBlock, JlcPart): - """ESP32C3 with 4MB integrated flash - TODO: support other part numbers, including without integrated flash - """ - RESOURCE_PIN_REMAP = { - 'GPIO0': '4', - 'GPIO1': '5', - 'GPIO3': '8', - 'MTMS': '9', # GPIO4 - 'MTDI': '10', # GPIO5 - 'MTCK': '12', # GPIO6 - 'MTDO': '13', # GPIO7 - 'GPIO10': '16', - 'GPIO18': '25', - 'GPIO19': '26', - } - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper(super()._system_pinmap()).remap({ - 'Vdd': ['31', '32'], # VDDA - 'Vss': ['33'], # 33 is EP - 'GPIO2': '6', - 'EN': '7', - 'GPIO8': '14', - 'GPIO9': '15', - 'RXD': '27', # U0RXD, GPIO20 - 'TXD': '28', # U0TXD, GPIO21 - }) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.lna_in = self.Port(Passive()) - - # chip power draw is modeled in self.pwr - self.vdd3p3 = self.Port(VoltageSink( # needs to be downstream of a filter - voltage_limits=(3.0, 3.6)*Volt, # section 4.2 - )) - self.vdd3p3_rtc = self.Port(VoltageSink( - voltage_limits=(3.0, 3.6)*Volt, # section 4.2 - )) - self.vdd3p3_cpu = self.Port(VoltageSink( - voltage_limits=(3.0, 3.6)*Volt, # section 4.2 - )) - self.vdd_spi = self.Port(VoltageSink( - voltage_limits=(3.0, 3.6)*Volt, # section 4.2 - )) - - # 10ppm requirement from ESP32-C3-WROOM schematic, and in ESP32 hardware design guidelines - self.xtal = self.Port(CrystalDriver(frequency_limits=40*MHertz(tol=10e-6), - voltage_out=self.pwr.link().voltage)) - - @override - def generate(self) -> None: - super().generate() - - pinning = self._make_pinning() - pinning.update({ - '1': self.lna_in, - '11': self.vdd3p3_rtc, - '17': self.vdd3p3_cpu, - '18': self.vdd_spi, - '2': self.vdd3p3, - '3': self.vdd3p3, - '30': self.xtal.xtal_in, - '29': self.xtal.xtal_out, - }) - - self.footprint( - 'U', 'Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.65x3.65mm', - pinning, - mfr='Espressif Systems', part='ESP32-C3FH4', - datasheet='https://www.espressif.com/sites/default/files/documentation/esp32-c3-wroom-02_datasheet_en.pdf', - ) - self.assign(self.lcsc_part, 'C2858491') - self.assign(self.actual_basic_part, False) - - -class Esp32c3(Microcontroller, Radiofrequency, HasEspProgramming, Resettable, Esp32c3_Interfaces, - WithCrystalGenerator, IoControllerPowerRequired, BaseIoControllerExportable, DiscreteRfWarning, - GeneratorBlock): - """ESP32-C3 application circuit, bare chip + RF circuits. - NOT RECOMMENDED - you will need to do your own RF layout, instead consider using the WROOM module.""" - - DEFAULT_CRYSTAL_FREQUENCY = 40*MHertz(tol=10e-6) - - def __init__(self) -> None: - super().__init__() - self.ic: Esp32c3_Device - self.generator_param(self.reset.is_connected()) - - self.io2_ext_connected: bool = False - self.io8_ext_connected: bool = False - - @override - def contents(self) -> None: - super().contents() - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.ic = imp.Block(Esp32c3_Device(pin_assigns=ArrayStringExpr())) - self.connect(self.pwr, self.ic.vdd3p3_rtc, self.ic.vdd3p3_cpu, self.ic.vdd_spi) - - self.connect(self.xtal_node, self.ic.xtal) - self.connect(self.program_uart_node, self.ic.uart0) - self.connect(self.program_en_node, self.ic.en) - self.connect(self.program_boot_node, self.ic.io9) - - self.vdd_bulk_cap = imp.Block(DecouplingCapacitor(10*uFarad(tol=0.2))) # C5 - self.vdda_cap0 = imp.Block(DecouplingCapacitor(1*uFarad(tol=0.2))) # C3 - self.vdda_cap1 = imp.Block(DecouplingCapacitor(10*nFarad(tol=0.2))) # C3 - self.vddrtc_cap = imp.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))) # C3 - self.vddcpu_cap = imp.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))) # C10 - self.vddspi_cap = imp.Block(DecouplingCapacitor(1*uFarad(tol=0.2))) # C11 - - self.ant = imp.Block(Antenna(frequency=(2402, 2484)*MHertz, impedance=50*Ohm(tol=0.1), power=(0, 0.126)*Watt)) - # expand the bandwidth to allow a lower Q and higher bandwidth - # TODO: more principled calculation of Q / bandwidth, voltage, current and tolerance - # 10% tolerance is roughly to support 5% off-nominal tolerance plus 5% component tolerance - (self.pi, ), _ = self.chain(self.ic.lna_in, - imp.Block(PiLowPassFilter((2402-200, 2484+200)*MHertz, 35*Ohm, 10*Ohm, 50*Ohm, - 0.10, self.pwr.link().voltage, (0, 0.1)*Amp)), - self.ant.a) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.vdd3p3_l_cap = imp.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2)))\ - .connected(pwr=self.pwr) # C6 - self.vdd3p3_cap = imp.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2)))\ - .connected(pwr=self.ic.vdd3p3) # C7 - DNP on ESP32-C3-WROOM schematic but 0.1uF on hardware design guide - self.vdd3p3_l = self.Block(SeriesPowerInductor( - inductance=2*nHenry(tol=0.2), - )).connected(self.pwr, self.ic.vdd3p3) - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.en) - else: - self.en_pull = self.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10*mSecond(tol=0.2))).connected( - gnd=self.gnd, pwr=self.pwr, io=self.ic.en) - - # Note strapping pins (section 3.3) IO2, 8, 9; IO9 is internally pulled up - # IO9 (internally pulled up) is 1 for SPI boot and 0 for download boot - # IO2 must be 1 for both SPI and download boot, while IO8 must be 1 for download boot - if not self.io8_ext_connected: - self.connect(self.ic.io8, self.pwr.as_digital_source()) - self.io8_ext_connected = True # set to ensure this runs after external connections - if not self.io2_ext_connected: - self.connect(self.ic.io2, self.pwr.as_digital_source()) - self.io2_ext_connected = True # set to ensure this runs after external connections - - ExportType = TypeVar('ExportType', bound=Port) - @override - def _make_export_vector(self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, - assign: Optional[str]) -> Optional[str]: - """Add support for _GPIO2/8/9_STRAP and remap them to io2/8/9.""" - if isinstance(self_io, DigitalBidir): - if assign == f'{name}=_GPIO2_STRAP_EXT_PU': - self.connect(self_io, self.ic.io2) - assert not self.io2_ext_connected # assert not yet hard tied - self.io2_ext_connected = True - return None - elif assign == f'{name}=_GPIO8_STRAP_EXT_PU': - self.connect(self_io, self.ic.io8) - assert not self.io8_ext_connected # assert not yet hard tied - self.io8_ext_connected = True - return None - elif assign == f'{name}=_GPIO9_STRAP': - self.connect(self_io, self.ic.io9) - return None - return super()._make_export_vector(self_io, inner_vector, name, assign) - - @override - def _crystal_required(self) -> bool: - return True # crystal oscillator always required - - -class Xiao_Esp32c3(IoControllerUsbOut, IoControllerPowerOut, Esp32c3_Ios, IoController, GeneratorBlock, - FootprintBlock): - """ESP32-C3 development board, a tiny development (21x17.5mm) daughterboard with a RISC-V microcontroller - supporting WiFi and BLE. Has an onboard USB connector, so this can also source power. - - Limited pins (only 11 for IOs, of which 6 are usable as the other 5 have boot requirements). - - Requires Seeed Studio's KiCad library for the footprint: https://github.com/Seeed-Studio/OPL_Kicad_Library - The 'Seeed Studio XIAO Series Library' must have been added as a footprint library of the same name. - - Pinning data: https://www.seeedstudio.com/blog/wp-content/uploads/2022/08/Seeed-Studio-XIAO-Series-Package-and-PCB-Design.pdf - """ - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'VDD': '12', - 'GND': '13', - 'VUSB': '14', - } - RESOURCE_PIN_REMAP = { - # 'GPIO2': '1', # boot pin, non-allocatable - 'GPIO3': '2', - 'MTMS': '3', - 'MTDI': '4', - 'MTCK': '5', - 'MTDO': '6', - # 'GPIO21': '7', # boot pin, non-allocatable - - # 'GPIO20': '8', # boot pin, non-allocatable - # 'GPIO8': '9', # boot pin, non-allocatable - # 'GPIO9': '10', # boot pin, non-allocatable - 'VDD_SPI': '11', - } - - @override - def _vddio(self) -> Port[VoltageLink]: - if self.get(self.pwr.is_connected()): # board sinks power - return self.pwr - else: - return self.pwr_out - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - if self.get(self.pwr.is_connected()): # board sinks power - self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") - self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") - return VariantPinRemapper({ - 'VDD': self.pwr, - 'GND': self.gnd, - }).remap(self.SYSTEM_PIN_REMAP) - else: # board sources power (default) - return VariantPinRemapper({ - 'VDD': self.pwr_out, - 'GND': self.gnd, - 'VUSB': self.vusb_out, - }).remap(self.SYSTEM_PIN_REMAP) - - @override - def contents(self) -> None: - super().contents() - - self.gnd.init_from(Ground()) - self.pwr.init_from(self._vdd_model()) - - self.vusb_out.init_from(VoltageSource( - voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - self.pwr_out.init_from(VoltageSource( - voltage_out=3.3*Volt(tol=0.05), # tolerance is a guess - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - - self.generator_param(self.pwr.is_connected()) - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', 'Seeed Studio XIAO Series Library:XIAO-ESP32C3-SMD', - self._make_pinning(), - mfr='', part='XIAO ESP32C3', - datasheet='https://www.seeedstudio.com/Seeed-XIAO-ESP32C3-p-5431.html' - ) + """ESP32C3 with 4MB integrated flash + TODO: support other part numbers, including without integrated flash + """ + + RESOURCE_PIN_REMAP = { + "GPIO0": "4", + "GPIO1": "5", + "GPIO3": "8", + "MTMS": "9", # GPIO4 + "MTDI": "10", # GPIO5 + "MTCK": "12", # GPIO6 + "MTDO": "13", # GPIO7 + "GPIO10": "16", + "GPIO18": "25", + "GPIO19": "26", + } + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return VariantPinRemapper(super()._system_pinmap()).remap( + { + "Vdd": ["31", "32"], # VDDA + "Vss": ["33"], # 33 is EP + "GPIO2": "6", + "EN": "7", + "GPIO8": "14", + "GPIO9": "15", + "RXD": "27", # U0RXD, GPIO20 + "TXD": "28", # U0TXD, GPIO21 + } + ) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.lna_in = self.Port(Passive()) + + # chip power draw is modeled in self.pwr + self.vdd3p3 = self.Port( + VoltageSink( # needs to be downstream of a filter + voltage_limits=(3.0, 3.6) * Volt, # section 4.2 + ) + ) + self.vdd3p3_rtc = self.Port( + VoltageSink( + voltage_limits=(3.0, 3.6) * Volt, # section 4.2 + ) + ) + self.vdd3p3_cpu = self.Port( + VoltageSink( + voltage_limits=(3.0, 3.6) * Volt, # section 4.2 + ) + ) + self.vdd_spi = self.Port( + VoltageSink( + voltage_limits=(3.0, 3.6) * Volt, # section 4.2 + ) + ) + + # 10ppm requirement from ESP32-C3-WROOM schematic, and in ESP32 hardware design guidelines + self.xtal = self.Port( + CrystalDriver(frequency_limits=40 * MHertz(tol=10e-6), voltage_out=self.pwr.link().voltage) + ) + + @override + def generate(self) -> None: + super().generate() + + pinning = self._make_pinning() + pinning.update( + { + "1": self.lna_in, + "11": self.vdd3p3_rtc, + "17": self.vdd3p3_cpu, + "18": self.vdd_spi, + "2": self.vdd3p3, + "3": self.vdd3p3, + "30": self.xtal.xtal_in, + "29": self.xtal.xtal_out, + } + ) + + self.footprint( + "U", + "Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.65x3.65mm", + pinning, + mfr="Espressif Systems", + part="ESP32-C3FH4", + datasheet="https://www.espressif.com/sites/default/files/documentation/esp32-c3-wroom-02_datasheet_en.pdf", + ) + self.assign(self.lcsc_part, "C2858491") + self.assign(self.actual_basic_part, False) + + +class Esp32c3( + Microcontroller, + Radiofrequency, + HasEspProgramming, + Resettable, + Esp32c3_Interfaces, + WithCrystalGenerator, + IoControllerPowerRequired, + BaseIoControllerExportable, + DiscreteRfWarning, + GeneratorBlock, +): + """ESP32-C3 application circuit, bare chip + RF circuits. + NOT RECOMMENDED - you will need to do your own RF layout, instead consider using the WROOM module.""" + + DEFAULT_CRYSTAL_FREQUENCY = 40 * MHertz(tol=10e-6) + + def __init__(self) -> None: + super().__init__() + self.ic: Esp32c3_Device + self.generator_param(self.reset.is_connected()) + + self.io2_ext_connected: bool = False + self.io8_ext_connected: bool = False + + @override + def contents(self) -> None: + super().contents() + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.ic = imp.Block(Esp32c3_Device(pin_assigns=ArrayStringExpr())) + self.connect(self.pwr, self.ic.vdd3p3_rtc, self.ic.vdd3p3_cpu, self.ic.vdd_spi) + + self.connect(self.xtal_node, self.ic.xtal) + self.connect(self.program_uart_node, self.ic.uart0) + self.connect(self.program_en_node, self.ic.en) + self.connect(self.program_boot_node, self.ic.io9) + + self.vdd_bulk_cap = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) # C5 + self.vdda_cap0 = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))) # C3 + self.vdda_cap1 = imp.Block(DecouplingCapacitor(10 * nFarad(tol=0.2))) # C3 + self.vddrtc_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) # C3 + self.vddcpu_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) # C10 + self.vddspi_cap = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))) # C11 + + self.ant = imp.Block( + Antenna(frequency=(2402, 2484) * MHertz, impedance=50 * Ohm(tol=0.1), power=(0, 0.126) * Watt) + ) + # expand the bandwidth to allow a lower Q and higher bandwidth + # TODO: more principled calculation of Q / bandwidth, voltage, current and tolerance + # 10% tolerance is roughly to support 5% off-nominal tolerance plus 5% component tolerance + (self.pi,), _ = self.chain( + self.ic.lna_in, + imp.Block( + PiLowPassFilter( + (2402 - 200, 2484 + 200) * MHertz, + 35 * Ohm, + 10 * Ohm, + 50 * Ohm, + 0.10, + self.pwr.link().voltage, + (0, 0.1) * Amp, + ) + ), + self.ant.a, + ) + + with self.implicit_connect(ImplicitConnect(self.gnd, [Common])) as imp: + self.vdd3p3_l_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(pwr=self.pwr) # C6 + self.vdd3p3_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected( + pwr=self.ic.vdd3p3 + ) # C7 - DNP on ESP32-C3-WROOM schematic but 0.1uF on hardware design guide + self.vdd3p3_l = self.Block( + SeriesPowerInductor( + inductance=2 * nHenry(tol=0.2), + ) + ).connected(self.pwr, self.ic.vdd3p3) + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.en) + else: + self.en_pull = self.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10 * mSecond(tol=0.2))).connected( + gnd=self.gnd, pwr=self.pwr, io=self.ic.en + ) + + # Note strapping pins (section 3.3) IO2, 8, 9; IO9 is internally pulled up + # IO9 (internally pulled up) is 1 for SPI boot and 0 for download boot + # IO2 must be 1 for both SPI and download boot, while IO8 must be 1 for download boot + if not self.io8_ext_connected: + self.connect(self.ic.io8, self.pwr.as_digital_source()) + self.io8_ext_connected = True # set to ensure this runs after external connections + if not self.io2_ext_connected: + self.connect(self.ic.io2, self.pwr.as_digital_source()) + self.io2_ext_connected = True # set to ensure this runs after external connections + + ExportType = TypeVar("ExportType", bound=Port) + + @override + def _make_export_vector( + self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, assign: Optional[str] + ) -> Optional[str]: + """Add support for _GPIO2/8/9_STRAP and remap them to io2/8/9.""" + if isinstance(self_io, DigitalBidir): + if assign == f"{name}=_GPIO2_STRAP_EXT_PU": + self.connect(self_io, self.ic.io2) + assert not self.io2_ext_connected # assert not yet hard tied + self.io2_ext_connected = True + return None + elif assign == f"{name}=_GPIO8_STRAP_EXT_PU": + self.connect(self_io, self.ic.io8) + assert not self.io8_ext_connected # assert not yet hard tied + self.io8_ext_connected = True + return None + elif assign == f"{name}=_GPIO9_STRAP": + self.connect(self_io, self.ic.io9) + return None + return super()._make_export_vector(self_io, inner_vector, name, assign) + + @override + def _crystal_required(self) -> bool: + return True # crystal oscillator always required + + +class Xiao_Esp32c3(IoControllerUsbOut, IoControllerPowerOut, Esp32c3_Ios, IoController, GeneratorBlock, FootprintBlock): + """ESP32-C3 development board, a tiny development (21x17.5mm) daughterboard with a RISC-V microcontroller + supporting WiFi and BLE. Has an onboard USB connector, so this can also source power. + + Limited pins (only 11 for IOs, of which 6 are usable as the other 5 have boot requirements). + + Requires Seeed Studio's KiCad library for the footprint: https://github.com/Seeed-Studio/OPL_Kicad_Library + The 'Seeed Studio XIAO Series Library' must have been added as a footprint library of the same name. + + Pinning data: https://www.seeedstudio.com/blog/wp-content/uploads/2022/08/Seeed-Studio-XIAO-Series-Package-and-PCB-Design.pdf + """ + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "VDD": "12", + "GND": "13", + "VUSB": "14", + } + RESOURCE_PIN_REMAP = { + # 'GPIO2': '1', # boot pin, non-allocatable + "GPIO3": "2", + "MTMS": "3", + "MTDI": "4", + "MTCK": "5", + "MTDO": "6", + # 'GPIO21': '7', # boot pin, non-allocatable + # 'GPIO20': '8', # boot pin, non-allocatable + # 'GPIO8': '9', # boot pin, non-allocatable + # 'GPIO9': '10', # boot pin, non-allocatable + "VDD_SPI": "11", + } + + @override + def _vddio(self) -> Port[VoltageLink]: + if self.get(self.pwr.is_connected()): # board sinks power + return self.pwr + else: + return self.pwr_out + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + if self.get(self.pwr.is_connected()): # board sinks power + self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") + self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") + return VariantPinRemapper( + { + "VDD": self.pwr, + "GND": self.gnd, + } + ).remap(self.SYSTEM_PIN_REMAP) + else: # board sources power (default) + return VariantPinRemapper( + { + "VDD": self.pwr_out, + "GND": self.gnd, + "VUSB": self.vusb_out, + } + ).remap(self.SYSTEM_PIN_REMAP) + + @override + def contents(self) -> None: + super().contents() + + self.gnd.init_from(Ground()) + self.pwr.init_from(self._vdd_model()) + + self.vusb_out.init_from( + VoltageSource(voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, current_limits=UsbConnector.USB2_CURRENT_LIMITS) + ) + self.pwr_out.init_from( + VoltageSource( + voltage_out=3.3 * Volt(tol=0.05), # tolerance is a guess + current_limits=UsbConnector.USB2_CURRENT_LIMITS, + ) + ) + + self.generator_param(self.pwr.is_connected()) + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + "Seeed Studio XIAO Series Library:XIAO-ESP32C3-SMD", + self._make_pinning(), + mfr="", + part="XIAO ESP32C3", + datasheet="https://www.seeedstudio.com/Seeed-XIAO-ESP32C3-p-5431.html", + ) diff --git a/edg/parts/Microcontroller_Esp32s3.py b/edg/parts/Microcontroller_Esp32s3.py index 93a8e58c9..3b21fc4de 100644 --- a/edg/parts/Microcontroller_Esp32s3.py +++ b/edg/parts/Microcontroller_Esp32s3.py @@ -9,399 +9,431 @@ @non_library -class Esp32s3_Interfaces(IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerTouchDriver, IoControllerCan, - IoControllerUsb, IoControllerI2s, IoControllerDvp8, IoControllerWifi, IoControllerBle, - BaseIoController): - """Defines base interfaces for ESP32S3 microcontrollers""" +class Esp32s3_Interfaces( + IoControllerSpiPeripheral, + IoControllerI2cTarget, + IoControllerTouchDriver, + IoControllerCan, + IoControllerUsb, + IoControllerI2s, + IoControllerDvp8, + IoControllerWifi, + IoControllerBle, + BaseIoController, +): + """Defines base interfaces for ESP32S3 microcontrollers""" @non_library class Esp32s3_Ios(Esp32s3_Interfaces, BaseIoControllerPinmapGenerator): - """IOs definitions independent of infrastructural (e.g. power) pins.""" - RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name - - @abstractmethod - def _vddio(self) -> Port[VoltageLink]: - """Returns VDDIO (can be VoltageSink or VoltageSource).""" - ... - - def _vdd_model(self) -> VoltageSink: - return VoltageSink( # assumes single-rail module - voltage_limits=(3.0, 3.6)*Volt, # table 4-2 - current_draw=(0.001, 355)*mAmp + self.io_current_draw.upper() # from power off (table 4-8) to RF working (table 12 on WROOM datasheet) - ) - - def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: - return DigitalBidir.from_supply( # table 4-4 - self.gnd, pwr, - voltage_limit_tolerance=(-0.3, 0.3) * Volt, - current_limits=(-28, 40)*mAmp, - input_threshold_factor=(0.25, 0.75), - pullup_capable=True, pulldown_capable=True, - ) - - @override - def _io_pinmap(self) -> PinMapUtil: - pwr = self._vddio() - dio_model = self._dio_model(pwr) - - adc_model = AnalogSink.from_supply( - self.gnd, pwr, - signal_limit_abs=(0, 2.9)*Volt, # table 4-5, effective ADC range at max attenuation - # TODO: impedance / leakage - not specified by datasheet - ) - - uart_model = UartPort(DigitalBidir.empty()) # section 3.5.5, up to 5Mbps - spi_model = SpiController(DigitalBidir.empty(), (0, 80) * MHertz) # section 3.5.2, 80MHz in controller, 60MHz in peripheral - spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), (0, 80) * MHertz) - i2c_model = I2cController(DigitalBidir.empty()) # section 3.5.6, 100/400kHz and up to 800kbit/s - i2c_target_model = I2cController(DigitalBidir.empty()) - touch_model = TouchDriver() - can_model = CanControllerPort(DigitalBidir.empty()) # aka TWAI, up to 1Mbit/s - i2s_model = I2sController(DigitalBidir.empty()) - dvp8_model = Dvp8Host(DigitalBidir.empty()) - - return PinMapUtil([ # table 2-1 for overview, table 3-3 for remapping, table 2-4 for ADC - # VDD3P3_RTC domain - # PinResource('GPIO0', {'GPIO0': self._dio_model}), # strapping pin, boot mode - PinResource('GPIO1', {'GPIO1': dio_model, 'ADC1_CH0': adc_model, 'TOUCH1': touch_model}), - PinResource('GPIO2', {'GPIO2': dio_model, 'ADC1_CH1': adc_model, 'TOUCH2': touch_model}), - # technically a strapping pin for JTAG control, but needs to be enabled by eFuse - PinResource('GPIO3', {'GPIO3': dio_model, 'ADC1_CH2': adc_model, 'TOUCH3': touch_model}), - PinResource('GPIO4', {'GPIO4': dio_model, 'ADC1_CH3': adc_model, 'TOUCH4': touch_model}), - PinResource('GPIO5', {'GPIO5': dio_model, 'ADC1_CH4': adc_model, 'TOUCH5': touch_model}), - PinResource('GPIO6', {'GPIO6': dio_model, 'ADC1_CH5': adc_model, 'TOUCH6': touch_model}), - PinResource('GPIO7', {'GPIO7': dio_model, 'ADC1_CH6': adc_model, 'TOUCH7': touch_model}), - PinResource('GPIO8', {'GPIO8': dio_model, 'ADC1_CH7': adc_model, 'TOUCH8': touch_model}), - PinResource('GPIO9', {'GPIO9': dio_model, 'ADC1_CH8': adc_model, 'TOUCH9': touch_model}), - PinResource('GPIO10', {'GPIO10': dio_model, 'ADC1_CH9': adc_model, 'TOUCH10': touch_model}), - # ADC2 pins can't be used simultaneously with WiFi (section 2.3.3) and are not allocatable - PinResource('GPIO11', {'GPIO11': dio_model, 'TOUCH11': touch_model}), # also ADC2_CH0 - PinResource('GPIO12', {'GPIO12': dio_model, 'TOUCH12': touch_model}), # also ADC2_CH1 - PinResource('GPIO13', {'GPIO13': dio_model, 'TOUCH13': touch_model}), # also ADC2_CH2 - PinResource('GPIO14', {'GPIO14': dio_model, 'TOUCH14': touch_model}), # also ADC2_CH3 - - PinResource('XTAL_32K_P', {'GPIO15': dio_model}), # also ADC2_CH4 - PinResource('XTAL_32K_N', {'GPIO16': dio_model}), # also ADC2_CH5 - - PinResource('GPIO17', {'GPIO17': dio_model}), # also ADC2_CH6 - PinResource('GPIO18', {'GPIO18': dio_model}), # also ADC2_CH7 - PinResource('GPIO19', {'GPIO19': dio_model}), # also ADC2_CH8 / USB_D- - PinResource('GPIO20', {'GPIO20': dio_model}), # also ADC2_CH9 / USB_D+ - PinResource('GPIO21', {'GPIO21': dio_model}), - - # VDD_SPI domain - # section 2.3.3, these are allocated for flash and should not be used - # PinResource('SPICS1', {'GPIO26': dio_model}), - # PinResource('SPIHD', {'GPIO27': dio_model}), - # PinResource('SPIWP', {'GPIO28': dio_model}), - # PinResource('SPICS0', {'GPIO29': dio_model}), - # PinResource('SPICLK', {'GPIO30': dio_model}), - # PinResource('SPIQ', {'GPIO31': dio_model}), - # PinResource('SPID', {'GPIO32': dio_model}), - - # VDD_SPI / VDD3P3_CPU domain - PinResource('SPICLK_N', {'GPIO48': dio_model}), # appendix A - PinResource('SPICLK_P', {'GPIO47': dio_model}), # appendix A - # these may be allocated for PSRAM and should not be used - # PinResource('GPIO33', {'GPIO33': dio_model}), - # PinResource('GPIO34', {'GPIO34': dio_model}), - # PinResource('GPIO35', {'GPIO35': dio_model}), - # PinResource('GPIO36', {'GPIO36': dio_model}), - # PinResource('GPIO37', {'GPIO37': dio_model}), - - # VDD3P3_CPU domain - PinResource('GPIO38', {'GPIO38': dio_model}), - PinResource('MTCK', {'GPIO39': dio_model}), - PinResource('MTDO', {'GPIO40': dio_model}), - PinResource('MTDI', {'GPIO41': dio_model}), - PinResource('MTMS', {'GPIO42': dio_model}), - - # PinResource('U0TXD', {'GPIO43': dio_model}), # for programming - # PinResource('U0RXD', {'GPIO44': dio_model}), # for programming - # PeripheralFixedResource('U0', uart_model, { - # 'tx': ['GPIO43'], 'rx': ['GPIO44'] - # }), - - # PinResource('GPIO45', {'GPIO45': dio_model}), # strapping pin, VDD_SPI power source - # PinResource('GPIO46', {'GPIO46': dio_model}), # strapping pin, boot mode, keep low - - PeripheralAnyResource('U1', uart_model), - PeripheralAnyResource('U2', uart_model), - PeripheralAnyResource('I2CEXT0', i2c_model), - PeripheralAnyResource('I2CEXT1', i2c_model), - PeripheralAnyResource('I2CEXT0_T', i2c_target_model), # TODO shared resource w/ I2C controller - PeripheralAnyResource('I2CEXT1_T', i2c_target_model), # TODO shared resource w/ I2C controller - # SPI0/1 may be used for (possibly on-chip) flash / PSRAM - PeripheralAnyResource('SPI2', spi_model), - PeripheralAnyResource('SPI3', spi_model), - PeripheralAnyResource('SPI2_P', spi_peripheral_model), # TODO shared resource w/ SPI controller - PeripheralAnyResource('SPI3_P', spi_peripheral_model), # TODO shared resource w/ SPI controller - PeripheralAnyResource('TWAI', can_model), - PeripheralAnyResource('I2S0', i2s_model), - PeripheralAnyResource('I2S1', i2s_model), - PeripheralAnyResource('DVP', dvp8_model), # TODO this also eats an I2S port, also available as 16-bit - - PeripheralFixedResource('USB', UsbDevicePort.empty(), { - 'dp': ['GPIO20'], 'dm': ['GPIO19'] - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) + """IOs definitions independent of infrastructural (e.g. power) pins.""" + + RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name + + @abstractmethod + def _vddio(self) -> Port[VoltageLink]: + """Returns VDDIO (can be VoltageSink or VoltageSource).""" + ... + + def _vdd_model(self) -> VoltageSink: + return VoltageSink( # assumes single-rail module + voltage_limits=(3.0, 3.6) * Volt, # table 4-2 + current_draw=(0.001, 355) * mAmp + + self.io_current_draw.upper(), # from power off (table 4-8) to RF working (table 12 on WROOM datasheet) + ) + + def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: + return DigitalBidir.from_supply( # table 4-4 + self.gnd, + pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_limits=(-28, 40) * mAmp, + input_threshold_factor=(0.25, 0.75), + pullup_capable=True, + pulldown_capable=True, + ) + + @override + def _io_pinmap(self) -> PinMapUtil: + pwr = self._vddio() + dio_model = self._dio_model(pwr) + + adc_model = AnalogSink.from_supply( + self.gnd, + pwr, + signal_limit_abs=(0, 2.9) * Volt, # table 4-5, effective ADC range at max attenuation + # TODO: impedance / leakage - not specified by datasheet + ) + + uart_model = UartPort(DigitalBidir.empty()) # section 3.5.5, up to 5Mbps + spi_model = SpiController( + DigitalBidir.empty(), (0, 80) * MHertz + ) # section 3.5.2, 80MHz in controller, 60MHz in peripheral + spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), (0, 80) * MHertz) + i2c_model = I2cController(DigitalBidir.empty()) # section 3.5.6, 100/400kHz and up to 800kbit/s + i2c_target_model = I2cController(DigitalBidir.empty()) + touch_model = TouchDriver() + can_model = CanControllerPort(DigitalBidir.empty()) # aka TWAI, up to 1Mbit/s + i2s_model = I2sController(DigitalBidir.empty()) + dvp8_model = Dvp8Host(DigitalBidir.empty()) + + return PinMapUtil( + [ # table 2-1 for overview, table 3-3 for remapping, table 2-4 for ADC + # VDD3P3_RTC domain + # PinResource('GPIO0', {'GPIO0': self._dio_model}), # strapping pin, boot mode + PinResource("GPIO1", {"GPIO1": dio_model, "ADC1_CH0": adc_model, "TOUCH1": touch_model}), + PinResource("GPIO2", {"GPIO2": dio_model, "ADC1_CH1": adc_model, "TOUCH2": touch_model}), + # technically a strapping pin for JTAG control, but needs to be enabled by eFuse + PinResource("GPIO3", {"GPIO3": dio_model, "ADC1_CH2": adc_model, "TOUCH3": touch_model}), + PinResource("GPIO4", {"GPIO4": dio_model, "ADC1_CH3": adc_model, "TOUCH4": touch_model}), + PinResource("GPIO5", {"GPIO5": dio_model, "ADC1_CH4": adc_model, "TOUCH5": touch_model}), + PinResource("GPIO6", {"GPIO6": dio_model, "ADC1_CH5": adc_model, "TOUCH6": touch_model}), + PinResource("GPIO7", {"GPIO7": dio_model, "ADC1_CH6": adc_model, "TOUCH7": touch_model}), + PinResource("GPIO8", {"GPIO8": dio_model, "ADC1_CH7": adc_model, "TOUCH8": touch_model}), + PinResource("GPIO9", {"GPIO9": dio_model, "ADC1_CH8": adc_model, "TOUCH9": touch_model}), + PinResource("GPIO10", {"GPIO10": dio_model, "ADC1_CH9": adc_model, "TOUCH10": touch_model}), + # ADC2 pins can't be used simultaneously with WiFi (section 2.3.3) and are not allocatable + PinResource("GPIO11", {"GPIO11": dio_model, "TOUCH11": touch_model}), # also ADC2_CH0 + PinResource("GPIO12", {"GPIO12": dio_model, "TOUCH12": touch_model}), # also ADC2_CH1 + PinResource("GPIO13", {"GPIO13": dio_model, "TOUCH13": touch_model}), # also ADC2_CH2 + PinResource("GPIO14", {"GPIO14": dio_model, "TOUCH14": touch_model}), # also ADC2_CH3 + PinResource("XTAL_32K_P", {"GPIO15": dio_model}), # also ADC2_CH4 + PinResource("XTAL_32K_N", {"GPIO16": dio_model}), # also ADC2_CH5 + PinResource("GPIO17", {"GPIO17": dio_model}), # also ADC2_CH6 + PinResource("GPIO18", {"GPIO18": dio_model}), # also ADC2_CH7 + PinResource("GPIO19", {"GPIO19": dio_model}), # also ADC2_CH8 / USB_D- + PinResource("GPIO20", {"GPIO20": dio_model}), # also ADC2_CH9 / USB_D+ + PinResource("GPIO21", {"GPIO21": dio_model}), + # VDD_SPI domain + # section 2.3.3, these are allocated for flash and should not be used + # PinResource('SPICS1', {'GPIO26': dio_model}), + # PinResource('SPIHD', {'GPIO27': dio_model}), + # PinResource('SPIWP', {'GPIO28': dio_model}), + # PinResource('SPICS0', {'GPIO29': dio_model}), + # PinResource('SPICLK', {'GPIO30': dio_model}), + # PinResource('SPIQ', {'GPIO31': dio_model}), + # PinResource('SPID', {'GPIO32': dio_model}), + # VDD_SPI / VDD3P3_CPU domain + PinResource("SPICLK_N", {"GPIO48": dio_model}), # appendix A + PinResource("SPICLK_P", {"GPIO47": dio_model}), # appendix A + # these may be allocated for PSRAM and should not be used + # PinResource('GPIO33', {'GPIO33': dio_model}), + # PinResource('GPIO34', {'GPIO34': dio_model}), + # PinResource('GPIO35', {'GPIO35': dio_model}), + # PinResource('GPIO36', {'GPIO36': dio_model}), + # PinResource('GPIO37', {'GPIO37': dio_model}), + # VDD3P3_CPU domain + PinResource("GPIO38", {"GPIO38": dio_model}), + PinResource("MTCK", {"GPIO39": dio_model}), + PinResource("MTDO", {"GPIO40": dio_model}), + PinResource("MTDI", {"GPIO41": dio_model}), + PinResource("MTMS", {"GPIO42": dio_model}), + # PinResource('U0TXD', {'GPIO43': dio_model}), # for programming + # PinResource('U0RXD', {'GPIO44': dio_model}), # for programming + # PeripheralFixedResource('U0', uart_model, { + # 'tx': ['GPIO43'], 'rx': ['GPIO44'] + # }), + # PinResource('GPIO45', {'GPIO45': dio_model}), # strapping pin, VDD_SPI power source + # PinResource('GPIO46', {'GPIO46': dio_model}), # strapping pin, boot mode, keep low + PeripheralAnyResource("U1", uart_model), + PeripheralAnyResource("U2", uart_model), + PeripheralAnyResource("I2CEXT0", i2c_model), + PeripheralAnyResource("I2CEXT1", i2c_model), + PeripheralAnyResource("I2CEXT0_T", i2c_target_model), # TODO shared resource w/ I2C controller + PeripheralAnyResource("I2CEXT1_T", i2c_target_model), # TODO shared resource w/ I2C controller + # SPI0/1 may be used for (possibly on-chip) flash / PSRAM + PeripheralAnyResource("SPI2", spi_model), + PeripheralAnyResource("SPI3", spi_model), + PeripheralAnyResource("SPI2_P", spi_peripheral_model), # TODO shared resource w/ SPI controller + PeripheralAnyResource("SPI3_P", spi_peripheral_model), # TODO shared resource w/ SPI controller + PeripheralAnyResource("TWAI", can_model), + PeripheralAnyResource("I2S0", i2s_model), + PeripheralAnyResource("I2S1", i2s_model), + PeripheralAnyResource("DVP", dvp8_model), # TODO this also eats an I2S port, also available as 16-bit + PeripheralFixedResource("USB", UsbDevicePort.empty(), {"dp": ["GPIO20"], "dm": ["GPIO19"]}), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) @abstract_block class Esp32s3_Base(Esp32s3_Ios, GeneratorBlock): - """Base class for ESP32-S3 series microcontrollers with WiFi and Bluetooth (classic and LE) - and AI acceleration + """Base class for ESP32-S3 series microcontrollers with WiFi and Bluetooth (classic and LE) + and AI acceleration - Chip datasheet: https://www.espressif.com/documentation/esp32-s3_datasheet_en.pdf - """ - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) + Chip datasheet: https://www.espressif.com/documentation/esp32-s3_datasheet_en.pdf + """ - @override - def _vddio(self) -> Port[VoltageLink]: - return self.pwr + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ - 'VDD': self.pwr, # including VDD3V3, VDD3P3_RTC, VDD_SPI, VDD3P3_CPU - 'CHIP_PU': self.chip_pu, - 'GND': self.gnd, + @override + def _vddio(self) -> Port[VoltageLink]: + return self.pwr - 'GPIO0': self.io0, - 'U0RXD': self.uart0.rx, - 'U0TXD': self.uart0.tx, - }).remap(self.SYSTEM_PIN_REMAP) + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return VariantPinRemapper( + { + "VDD": self.pwr, # including VDD3V3, VDD3P3_RTC, VDD_SPI, VDD3P3_CPU + "CHIP_PU": self.chip_pu, + "GND": self.gnd, + "GPIO0": self.io0, + "U0RXD": self.uart0.rx, + "U0TXD": self.uart0.tx, + } + ).remap(self.SYSTEM_PIN_REMAP) - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) - self.pwr = self.Port(self._vdd_model(), [Power]) - self.gnd = self.Port(Ground(), [Common]) + self.pwr = self.Port(self._vdd_model(), [Power]) + self.gnd = self.Port(Ground(), [Common]) - dio_model = self._dio_model(self.pwr) - self.chip_pu = self.Port(dio_model) # table 2-5, power up/down control, do NOT leave floating - self.io0 = self.Port(dio_model, optional=True) # table 2-11, default pullup (SPI boot), set low to download boot - self.uart0 = self.Port(UartPort(dio_model), optional=True) # programming + dio_model = self._dio_model(self.pwr) + self.chip_pu = self.Port(dio_model) # table 2-5, power up/down control, do NOT leave floating + self.io0 = self.Port( + dio_model, optional=True + ) # table 2-11, default pullup (SPI boot), set low to download boot + self.uart0 = self.Port(UartPort(dio_model), optional=True) # programming class Esp32s3_Wroom_1_Device(Esp32s3_Base, InternalSubcircuit, FootprintBlock, JlcPart): - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'VDD': '2', - 'GND': ['1', '40', '41'], # 41 is EP - 'CHIP_PU': '3', # aka EN - - 'GPIO0': '27', - - 'U0RXD': '36', - 'U0TXD': '37', - } - - RESOURCE_PIN_REMAP = { - 'GPIO4': '4', - 'GPIO5': '5', - 'GPIO6': '6', - 'GPIO7': '7', - 'XTAL_32K_P': '8', # GPIO15 - 'XTAL_32K_N': '9', # GPIO16 - 'GPIO17': '10', - 'GPIO18': '11', - 'GPIO8': '12', - 'GPIO19': '13', - 'GPIO20': '14', - 'GPIO3': '15', - # 'GPIO46': '16', # strapping pin - 'GPIO9': '17', - 'GPIO10': '18', - 'GPIO11': '19', - 'GPIO12': '20', - 'GPIO13': '21', - 'GPIO14': '22', - 'GPIO21': '23', - 'SPICLK_P': '24', # GPIO47 - 'SPICLK_N': '25', # GPIO48 - # 'GPIO45': '26', # strapping pin - # 'GPIO35': '28', # not available on PSRAM variants - # 'GPIO36': '29', # not available on PSRAM variants - # 'GPIO37': '30', # not available on PSRAM variants - 'GPIO38': '31', - 'MTCK': '32', # GPIO39 - 'MTDO': '33', # GPIO40 - 'MTDI': '34', # GPIO41 - 'MTMS': '35', # GPIO42 - 'GPIO2': '38', - 'GPIO1': '39', - } - - @override - def generate(self) -> None: - super().generate() - - self.assign(self.lcsc_part, 'C2913202') # note standard only assembly - self.assign(self.actual_basic_part, False) - self.footprint( - 'U', 'RF_Module:ESP32-S3-WROOM-1', - self._make_pinning(), - mfr='Espressif Systems', part='ESP32-S3-WROOM-1-N16R8', # higher end variant - datasheet='https://www.espressif.com/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf', - ) - - -class Esp32s3_Wroom_1(Microcontroller, Radiofrequency, HasEspProgramming, Resettable, Esp32s3_Interfaces, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): - """ESP32-S3-WROOM-1 module - """ - def __init__(self) -> None: - super().__init__() - self.ic: Esp32s3_Wroom_1_Device - self.generator_param(self.reset.is_connected()) - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.ic = imp.Block(Esp32s3_Wroom_1_Device(pin_assigns=ArrayStringExpr())) - self.connect(self.program_uart_node, self.ic.uart0) - self.connect(self.program_en_node, self.ic.chip_pu) - self.connect(self.program_boot_node, self.ic.io0) - - self.vcc_cap0 = imp.Block(DecouplingCapacitor(22 * uFarad(tol=0.2))) # C1 - self.vcc_cap1 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) # C2 - - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.chip_pu) - else: - self.en_pull = self.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10*mSecond(tol=0.2))).connected( - gnd=self.gnd, pwr=self.pwr, io=self.ic.chip_pu) - - ExportType = TypeVar('ExportType', bound=Port) - @override - def _make_export_vector(self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, - assign: Optional[str]) -> Optional[str]: - """Allow overloading strapping pins""" - if isinstance(self_io, DigitalBidir): - if assign == f'{name}=_GPIO0_STRAP': - self.connect(self_io, self.ic.io0) - return None - return super()._make_export_vector(self_io, inner_vector, name, assign) - - -class Freenove_Esp32s3_Wroom(IoControllerUsbOut, IoControllerPowerOut, Esp32s3_Ios, IoController, GeneratorBlock, - FootprintBlock): - """Freenove ESP32S3 WROOM breakout breakout with camera. - - Board pinning: https://github.com/Freenove/Freenove_ESP32_S3_WROOM_Board/blob/main/ESP32S3_Pinout.png - - Top left is pin 1, going down the left side then up the right side. - Up is defined from the text orientation (antenna is on top). - """ - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'VDD': '1', - 'GND': '21', - 'VUSB': '20', - } - RESOURCE_PIN_REMAP = { - # 'GPIO4': '3', # CAM_SIOD - # 'GPIO5': '4', # CAM_SIOC - # 'GPIO6': '5', # CAM_VYSNC - # 'GPIO7': '6', # CAM_HREF - # 'GPIO15': '7', # CAM_XCLK - # 'GPIO16': '8', # CAM_Y9 - # 'GPIO17': '9', # CAM_Y8 - # 'GPIO18': '10', # CAM_Y7 - # 'GPIO8': '11', # CAM_Y4 - 'GPIO3': '12', - # 'GPIO46': '13', # strapping pin, boot mode - # 'GPIO9': '14', # CAM_Y3 - # 'GPIO10': '15', # CAM_Y5 - # 'GPIO11': '16', # CAM_Y2 - # 'GPIO12': '17', # CAM_Y6 - # 'GPIO13': '18', # CAM_PCLK - 'GPIO14': '19', - - # 'GPIO19': '22', # USB_D+ - # 'GPIO20': '23', # USB_D- - 'GPIO21': '24', - 'SPICLK_P': '25', # GPIO47 - 'SPICLK_N': '26', # GPIO48, internal WS2812 - # 'GPIO45': '27', # strapping pin, VDD_SPI - # 'GPIO35': '29', # PSRAM - # 'GPIO36': '30', # PSRAM - # 'GPIO37': '31', # PSRAM - # 'GPIO38': '32', # SD_CMD - # 'GPIO39': '33', # SD_CLK - # 'GPIO40': '34', # SD_DATA - 'MTDI': '35', # GPIO41 - 'MTMS': '36', # GPIO42 - 'GPIO2': '37', # internal LED - 'GPIO1': '38', - } - - @override - def _vddio(self) -> Port[VoltageLink]: - if self.get(self.pwr.is_connected()): # board sinks power - return self.pwr - else: - return self.pwr_out - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - if self.get(self.pwr.is_connected()): # board sinks power - self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") - self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") - return VariantPinRemapper({ - 'VDD': self.pwr, - 'GND': self.gnd, - }).remap(self.SYSTEM_PIN_REMAP) - else: # board sources power (default) - return VariantPinRemapper({ - 'VDD': self.pwr_out, - 'GND': self.gnd, - 'VUSB': self.vusb_out, - }).remap(self.SYSTEM_PIN_REMAP) - - @override - def _io_pinmap(self) -> PinMapUtil: # allow the camera I2C pins to be used externally - pwr = self._vddio() - return super()._io_pinmap().add([ - PeripheralFixedPin('CAM_SCCB', I2cController(self._dio_model(pwr), has_pullup=True), { - 'scl': '4', 'sda': '3' - }) - ]) - - @override - def contents(self) -> None: - super().contents() - - self.gnd.init_from(Ground()) - self.pwr.init_from(self._vdd_model()) - - self.vusb_out.init_from(VoltageSource( - voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - self.pwr_out.init_from(VoltageSource( - voltage_out=3.3*Volt(tol=0.05), # tolerance is a guess - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - - self.generator_param(self.pwr.is_connected()) - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', 'edg:Freenove_ESP32-WROVER', - self._make_pinning(), - mfr='', part='Freenove ESP32S3-WROOM', - ) + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "VDD": "2", + "GND": ["1", "40", "41"], # 41 is EP + "CHIP_PU": "3", # aka EN + "GPIO0": "27", + "U0RXD": "36", + "U0TXD": "37", + } + + RESOURCE_PIN_REMAP = { + "GPIO4": "4", + "GPIO5": "5", + "GPIO6": "6", + "GPIO7": "7", + "XTAL_32K_P": "8", # GPIO15 + "XTAL_32K_N": "9", # GPIO16 + "GPIO17": "10", + "GPIO18": "11", + "GPIO8": "12", + "GPIO19": "13", + "GPIO20": "14", + "GPIO3": "15", + # 'GPIO46': '16', # strapping pin + "GPIO9": "17", + "GPIO10": "18", + "GPIO11": "19", + "GPIO12": "20", + "GPIO13": "21", + "GPIO14": "22", + "GPIO21": "23", + "SPICLK_P": "24", # GPIO47 + "SPICLK_N": "25", # GPIO48 + # 'GPIO45': '26', # strapping pin + # 'GPIO35': '28', # not available on PSRAM variants + # 'GPIO36': '29', # not available on PSRAM variants + # 'GPIO37': '30', # not available on PSRAM variants + "GPIO38": "31", + "MTCK": "32", # GPIO39 + "MTDO": "33", # GPIO40 + "MTDI": "34", # GPIO41 + "MTMS": "35", # GPIO42 + "GPIO2": "38", + "GPIO1": "39", + } + + @override + def generate(self) -> None: + super().generate() + + self.assign(self.lcsc_part, "C2913202") # note standard only assembly + self.assign(self.actual_basic_part, False) + self.footprint( + "U", + "RF_Module:ESP32-S3-WROOM-1", + self._make_pinning(), + mfr="Espressif Systems", + part="ESP32-S3-WROOM-1-N16R8", # higher end variant + datasheet="https://www.espressif.com/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf", + ) + + +class Esp32s3_Wroom_1( + Microcontroller, + Radiofrequency, + HasEspProgramming, + Resettable, + Esp32s3_Interfaces, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): + """ESP32-S3-WROOM-1 module""" + + def __init__(self) -> None: + super().__init__() + self.ic: Esp32s3_Wroom_1_Device + self.generator_param(self.reset.is_connected()) + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.ic = imp.Block(Esp32s3_Wroom_1_Device(pin_assigns=ArrayStringExpr())) + self.connect(self.program_uart_node, self.ic.uart0) + self.connect(self.program_en_node, self.ic.chip_pu) + self.connect(self.program_boot_node, self.ic.io0) + + self.vcc_cap0 = imp.Block(DecouplingCapacitor(22 * uFarad(tol=0.2))) # C1 + self.vcc_cap1 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) # C2 + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.chip_pu) + else: + self.en_pull = self.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10 * mSecond(tol=0.2))).connected( + gnd=self.gnd, pwr=self.pwr, io=self.ic.chip_pu + ) + + ExportType = TypeVar("ExportType", bound=Port) + + @override + def _make_export_vector( + self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, assign: Optional[str] + ) -> Optional[str]: + """Allow overloading strapping pins""" + if isinstance(self_io, DigitalBidir): + if assign == f"{name}=_GPIO0_STRAP": + self.connect(self_io, self.ic.io0) + return None + return super()._make_export_vector(self_io, inner_vector, name, assign) + + +class Freenove_Esp32s3_Wroom( + IoControllerUsbOut, IoControllerPowerOut, Esp32s3_Ios, IoController, GeneratorBlock, FootprintBlock +): + """Freenove ESP32S3 WROOM breakout breakout with camera. + + Board pinning: https://github.com/Freenove/Freenove_ESP32_S3_WROOM_Board/blob/main/ESP32S3_Pinout.png + + Top left is pin 1, going down the left side then up the right side. + Up is defined from the text orientation (antenna is on top). + """ + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "VDD": "1", + "GND": "21", + "VUSB": "20", + } + RESOURCE_PIN_REMAP = { + # 'GPIO4': '3', # CAM_SIOD + # 'GPIO5': '4', # CAM_SIOC + # 'GPIO6': '5', # CAM_VYSNC + # 'GPIO7': '6', # CAM_HREF + # 'GPIO15': '7', # CAM_XCLK + # 'GPIO16': '8', # CAM_Y9 + # 'GPIO17': '9', # CAM_Y8 + # 'GPIO18': '10', # CAM_Y7 + # 'GPIO8': '11', # CAM_Y4 + "GPIO3": "12", + # 'GPIO46': '13', # strapping pin, boot mode + # 'GPIO9': '14', # CAM_Y3 + # 'GPIO10': '15', # CAM_Y5 + # 'GPIO11': '16', # CAM_Y2 + # 'GPIO12': '17', # CAM_Y6 + # 'GPIO13': '18', # CAM_PCLK + "GPIO14": "19", + # 'GPIO19': '22', # USB_D+ + # 'GPIO20': '23', # USB_D- + "GPIO21": "24", + "SPICLK_P": "25", # GPIO47 + "SPICLK_N": "26", # GPIO48, internal WS2812 + # 'GPIO45': '27', # strapping pin, VDD_SPI + # 'GPIO35': '29', # PSRAM + # 'GPIO36': '30', # PSRAM + # 'GPIO37': '31', # PSRAM + # 'GPIO38': '32', # SD_CMD + # 'GPIO39': '33', # SD_CLK + # 'GPIO40': '34', # SD_DATA + "MTDI": "35", # GPIO41 + "MTMS": "36", # GPIO42 + "GPIO2": "37", # internal LED + "GPIO1": "38", + } + + @override + def _vddio(self) -> Port[VoltageLink]: + if self.get(self.pwr.is_connected()): # board sinks power + return self.pwr + else: + return self.pwr_out + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + if self.get(self.pwr.is_connected()): # board sinks power + self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") + self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") + return VariantPinRemapper( + { + "VDD": self.pwr, + "GND": self.gnd, + } + ).remap(self.SYSTEM_PIN_REMAP) + else: # board sources power (default) + return VariantPinRemapper( + { + "VDD": self.pwr_out, + "GND": self.gnd, + "VUSB": self.vusb_out, + } + ).remap(self.SYSTEM_PIN_REMAP) + + @override + def _io_pinmap(self) -> PinMapUtil: # allow the camera I2C pins to be used externally + pwr = self._vddio() + return ( + super() + ._io_pinmap() + .add( + [ + PeripheralFixedPin( + "CAM_SCCB", I2cController(self._dio_model(pwr), has_pullup=True), {"scl": "4", "sda": "3"} + ) + ] + ) + ) + + @override + def contents(self) -> None: + super().contents() + + self.gnd.init_from(Ground()) + self.pwr.init_from(self._vdd_model()) + + self.vusb_out.init_from( + VoltageSource(voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, current_limits=UsbConnector.USB2_CURRENT_LIMITS) + ) + self.pwr_out.init_from( + VoltageSource( + voltage_out=3.3 * Volt(tol=0.05), # tolerance is a guess + current_limits=UsbConnector.USB2_CURRENT_LIMITS, + ) + ) + + self.generator_param(self.pwr.is_connected()) + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + "edg:Freenove_ESP32-WROVER", + self._make_pinning(), + mfr="", + part="Freenove ESP32S3-WROOM", + ) diff --git a/edg/parts/Microcontroller_Lpc1549.py b/edg/parts/Microcontroller_Lpc1549.py index 63b11666b..c838a004d 100644 --- a/edg/parts/Microcontroller_Lpc1549.py +++ b/edg/parts/Microcontroller_Lpc1549.py @@ -7,398 +7,426 @@ @non_library -class Lpc1549Base_Device(IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerDac, IoControllerCan, - IoControllerUsb, BaseIoControllerPinmapGenerator, InternalSubcircuit, GeneratorBlock, JlcPart, - FootprintBlock): - PACKAGE: str # package name for footprint(...) - PART: str # part name for footprint(...) - LCSC_PART: str - - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) - RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - # Ports with shared references - self.pwr = self.Port(VoltageSink( - voltage_limits=(2.4, 3.6) * Volt, - current_draw=(0, 19)*mAmp + self.io_current_draw.upper(), # rough guesstimate from Figure 11.1 for supply Idd (active mode) - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - - # Additional ports (on top of IoController) - # Crystals from table 15, 32, 33 - # TODO Table 32, model crystal load capacitance and series resistance ratings - self.xtal = self.Port(CrystalDriver(frequency_limits=(1, 25)*MHertz, voltage_out=self.pwr.link().voltage), - optional=True) - # Assumed from "32kHz crystal" in 14.5 - self.xtal_rtc = self.Port(CrystalDriver(frequency_limits=(32, 33)*kHertz, voltage_out=self.pwr.link().voltage), - optional=True) - - self.swd = self.Port(SwdTargetPort.empty()) - self.reset = self.Port(DigitalSink.empty(), optional=True) # internally pulled up, TODO disable-able and assignable as GPIO - self._io_ports.insert(0, self.swd) - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ - 'VddA': self.pwr, - 'VssA': self.gnd, - 'VrefP_ADC': self.pwr, - 'VrefP_DAC': self.pwr, - 'VrefN': self.gnd, - 'Vbat': self.pwr, - 'Vss': self.gnd, - 'Vdd': self.pwr, - - 'XTALIN': self.xtal.xtal_in, # TODO Table 3, note 11, float/gnd (gnd preferred) if not used - 'XTALOUT': self.xtal.xtal_out, # TODO Table 3, note 11, float if not used - 'RTCXIN': self.xtal_rtc.xtal_in, # 14.5 can be grounded if RTC not used - 'RTCXOUT': self.xtal_rtc.xtal_out, - 'RESET': self.reset - }).remap(self.SYSTEM_PIN_REMAP) - - @override - def _io_pinmap(self) -> PinMapUtil: - # Port models - dio_5v_model = DigitalBidir.from_supply( - self.gnd, self.pwr, - voltage_limit_abs=(0, 5) * Volt, - current_limits=(-50, 45) * mAmp, - input_threshold_factor=(0.3, 0.7), - pullup_capable=True, pulldown_capable=True - ) - dio_non5v_model = DigitalBidir.from_supply( # only used when overlapped w/ DAC PIO0_12 - self.gnd, self.pwr, # up to VddA - voltage_limit_tolerance=(0, 0) * Volt, - current_limits=(-50, 45) * mAmp, - input_threshold_factor=(0.3, 0.7), - pullup_capable=True, pulldown_capable=True - ) - dio_highcurrrent_model = DigitalBidir.from_supply( # only used for PIO0_24 - self.gnd, self.pwr, - voltage_limit_abs=(0, 5) * Volt, - current_limits=(-50, 20) * mAmp, # TODO: 12mA when Vdd < 2.7V - input_threshold_factor=(0.3, 0.7), - pullup_capable=True, pulldown_capable=True - ) - - adc_model = AnalogSink.from_supply( - self.gnd, self.pwr, - voltage_limit_tolerance=(0, 0), # assumed from DIO model - impedance=(100, float('inf')) * kOhm - ) - dac_model = AnalogSource.from_supply( - self.gnd, self.pwr, - signal_out_bound=(0, -0.3*Volt), - current_limits=RangeExpr.ALL, # not given by spec - impedance=(300, 300) * Ohm # Table 25, "typical" rating - ) - - self.reset.init_from(DigitalSink.from_bidir(dio_5v_model)) - uart_model = UartPort(DigitalBidir.empty()) - spi_model = SpiController(DigitalBidir.empty()) - spi_peripheral_model = SpiPeripheral(DigitalBidir.empty()) # MISO driven when CS asserted - - return PinMapUtil([ # partial table for 48- and 64-pin only - PinResource('PIO0_0', {'PIO0_0': dio_5v_model, 'ADC0_10': adc_model}), - PinResource('PIO0_1', {'PIO0_1': dio_5v_model, 'ADC0_7': adc_model}), - PinResource('PIO0_2', {'PIO0_2': dio_5v_model, 'ADC0_6': adc_model}), - PinResource('PIO0_3', {'PIO0_3': dio_5v_model, 'ADC0_5': adc_model}), - PinResource('PIO0_4', {'PIO0_4': dio_5v_model, 'ADC0_4': adc_model}), - PinResource('PIO0_5', {'PIO0_5': dio_5v_model, 'ADC0_3': adc_model}), - PinResource('PIO0_6', {'PIO0_6': dio_5v_model, 'ADC0_2': adc_model}), - PinResource('PIO0_7', {'PIO0_7': dio_5v_model, 'ADC0_1': adc_model}), - - PinResource('PIO0_8', {'PIO0_8': dio_5v_model, 'ADC0_0': adc_model}), - PinResource('PIO0_9', {'PIO0_9': dio_5v_model, 'ADC1_1': adc_model}), - PinResource('PIO0_10', {'PIO0_10': dio_5v_model, 'ADC1_2': adc_model}), - PinResource('PIO0_11', {'PIO0_11': dio_5v_model, 'ADC1_3': adc_model}), - PinResource('PIO0_12', {'PIO0_12': dio_non5v_model, 'DAC_OUT': dac_model}), - PinResource('PIO0_13', {'PIO0_13': dio_5v_model, 'ADC1_6': adc_model}), - PinResource('PIO0_14', {'PIO0_14': dio_5v_model, 'ADC1_7': adc_model}), - PinResource('PIO0_15', {'PIO0_15': dio_5v_model, 'ADC1_8': adc_model}), - PinResource('PIO0_16', {'PIO0_16': dio_5v_model, 'ADC1_9': adc_model}), - PinResource('PIO0_17', {'PIO0_17': dio_5v_model}), - - PinResource('PIO0_18', {'PIO0_18': dio_5v_model}), - PinResource('PIO0_19', {'PIO0_19': dio_5v_model}), - PinResource('PIO0_20', {'PIO0_20': dio_5v_model}), - PinResource('PIO0_21', {'PIO0_21': dio_5v_model}), # also RESET - PinResource('PIO0_22', {'PIO0_22': dio_5v_model}), - PinResource('PIO0_23', {'PIO0_23': dio_5v_model}), - PinResource('PIO0_24', {'PIO0_24': dio_highcurrrent_model}), - PinResource('PIO0_25', {'PIO0_25': dio_5v_model}), - PinResource('PIO0_26', {'PIO0_26': dio_5v_model}), - - PinResource('PIO0_27', {'PIO0_27': dio_5v_model}), - PinResource('PIO0_28', {'PIO0_28': dio_5v_model}), - PinResource('PIO0_29', {'PIO0_29': dio_5v_model}), - PinResource('PIO0_30', {'PIO0_30': dio_5v_model, 'ADC0_11': adc_model}), - PinResource('PIO0_31', {'PIO0_31': dio_5v_model, 'ADC0_9': adc_model}), - PinResource('PIO1_0', {'PIO1_0': dio_5v_model, 'ADC0_8': adc_model}), - PinResource('PIO1_1', {'PIO1_1': dio_5v_model, 'ADC1_0': adc_model}), - PinResource('PIO1_2', {'PIO1_2': dio_5v_model, 'ADC1_4': adc_model}), - PinResource('PIO1_3', {'PIO1_3': dio_5v_model, 'ADC1_5': adc_model}), - PinResource('PIO1_4', {'PIO1_4': dio_5v_model, 'ADC1_10': adc_model}), - PinResource('PIO1_5', {'PIO1_5': dio_5v_model, 'ADC1_11': adc_model}), - - - PinResource('PIO1_6', {'PIO1_6': dio_5v_model}), - PinResource('PIO1_7', {'PIO1_7': dio_5v_model}), - PinResource('PIO1_8', {'PIO1_8': dio_5v_model}), - PinResource('PIO1_9', {'PIO1_9': dio_5v_model}), - PinResource('PIO1_10', {'PIO1_10': dio_5v_model}), - PinResource('PIO1_11', {'PIO1_11': dio_5v_model}), - - PeripheralAnyResource('UART0', uart_model), - PeripheralAnyResource('UART1', uart_model), - PeripheralAnyResource('UART2', uart_model), - PeripheralAnyResource('SPI0', spi_model), - PeripheralAnyResource('SPI1', spi_model), - PeripheralAnyResource('SPI0_P', spi_peripheral_model), # TODO shared resource w/ SPI controller - PeripheralAnyResource('SPI1_P', spi_peripheral_model), # TODO shared resource w/ SPI controller - PeripheralAnyResource('CAN0', CanControllerPort(DigitalBidir.empty())), - - PeripheralFixedResource('I2C0', I2cController(DigitalBidir.empty()), { - 'scl': ['PIO0_22'], 'sda': ['PIO0_23'] - }), - PeripheralFixedResource('I2C0_T', I2cTarget(DigitalBidir.empty()), { # TODO shared resource w/ I2C controller - 'scl': ['PIO0_22'], 'sda': ['PIO0_23'] - }), - PeripheralFixedPin('USB', UsbDevicePort(), { - 'dp': 'USB_DP', 'dm': 'USB_DM' - }), - - # Figure 49: requires a pull-up on SWDIO and pull-down on SWCLK - PeripheralFixedResource('SWD', SwdTargetPort(DigitalBidir.empty()), { - 'swclk': ['PIO0_19'], 'swdio': ['PIO0_20'], - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', self.PACKAGE, - self._make_pinning(), - mfr='NXP', part=self.PART, - datasheet='https://www.nxp.com/docs/en/data-sheet/LPC15XX.pdf' - ) - self.assign(self.lcsc_part, self.LCSC_PART) - self.assign(self.actual_basic_part, False) +class Lpc1549Base_Device( + IoControllerSpiPeripheral, + IoControllerI2cTarget, + IoControllerDac, + IoControllerCan, + IoControllerUsb, + BaseIoControllerPinmapGenerator, + InternalSubcircuit, + GeneratorBlock, + JlcPart, + FootprintBlock, +): + PACKAGE: str # package name for footprint(...) + PART: str # part name for footprint(...) + LCSC_PART: str + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) + RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + # Ports with shared references + self.pwr = self.Port( + VoltageSink( + voltage_limits=(2.4, 3.6) * Volt, + current_draw=(0, 19) * mAmp + + self.io_current_draw.upper(), # rough guesstimate from Figure 11.1 for supply Idd (active mode) + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + + # Additional ports (on top of IoController) + # Crystals from table 15, 32, 33 + # TODO Table 32, model crystal load capacitance and series resistance ratings + self.xtal = self.Port( + CrystalDriver(frequency_limits=(1, 25) * MHertz, voltage_out=self.pwr.link().voltage), optional=True + ) + # Assumed from "32kHz crystal" in 14.5 + self.xtal_rtc = self.Port( + CrystalDriver(frequency_limits=(32, 33) * kHertz, voltage_out=self.pwr.link().voltage), optional=True + ) + + self.swd = self.Port(SwdTargetPort.empty()) + self.reset = self.Port( + DigitalSink.empty(), optional=True + ) # internally pulled up, TODO disable-able and assignable as GPIO + self._io_ports.insert(0, self.swd) + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return VariantPinRemapper( + { + "VddA": self.pwr, + "VssA": self.gnd, + "VrefP_ADC": self.pwr, + "VrefP_DAC": self.pwr, + "VrefN": self.gnd, + "Vbat": self.pwr, + "Vss": self.gnd, + "Vdd": self.pwr, + "XTALIN": self.xtal.xtal_in, # TODO Table 3, note 11, float/gnd (gnd preferred) if not used + "XTALOUT": self.xtal.xtal_out, # TODO Table 3, note 11, float if not used + "RTCXIN": self.xtal_rtc.xtal_in, # 14.5 can be grounded if RTC not used + "RTCXOUT": self.xtal_rtc.xtal_out, + "RESET": self.reset, + } + ).remap(self.SYSTEM_PIN_REMAP) + + @override + def _io_pinmap(self) -> PinMapUtil: + # Port models + dio_5v_model = DigitalBidir.from_supply( + self.gnd, + self.pwr, + voltage_limit_abs=(0, 5) * Volt, + current_limits=(-50, 45) * mAmp, + input_threshold_factor=(0.3, 0.7), + pullup_capable=True, + pulldown_capable=True, + ) + dio_non5v_model = DigitalBidir.from_supply( # only used when overlapped w/ DAC PIO0_12 + self.gnd, + self.pwr, # up to VddA + voltage_limit_tolerance=(0, 0) * Volt, + current_limits=(-50, 45) * mAmp, + input_threshold_factor=(0.3, 0.7), + pullup_capable=True, + pulldown_capable=True, + ) + dio_highcurrrent_model = DigitalBidir.from_supply( # only used for PIO0_24 + self.gnd, + self.pwr, + voltage_limit_abs=(0, 5) * Volt, + current_limits=(-50, 20) * mAmp, # TODO: 12mA when Vdd < 2.7V + input_threshold_factor=(0.3, 0.7), + pullup_capable=True, + pulldown_capable=True, + ) + + adc_model = AnalogSink.from_supply( + self.gnd, + self.pwr, + voltage_limit_tolerance=(0, 0), # assumed from DIO model + impedance=(100, float("inf")) * kOhm, + ) + dac_model = AnalogSource.from_supply( + self.gnd, + self.pwr, + signal_out_bound=(0, -0.3 * Volt), + current_limits=RangeExpr.ALL, # not given by spec + impedance=(300, 300) * Ohm, # Table 25, "typical" rating + ) + + self.reset.init_from(DigitalSink.from_bidir(dio_5v_model)) + uart_model = UartPort(DigitalBidir.empty()) + spi_model = SpiController(DigitalBidir.empty()) + spi_peripheral_model = SpiPeripheral(DigitalBidir.empty()) # MISO driven when CS asserted + + return PinMapUtil( + [ # partial table for 48- and 64-pin only + PinResource("PIO0_0", {"PIO0_0": dio_5v_model, "ADC0_10": adc_model}), + PinResource("PIO0_1", {"PIO0_1": dio_5v_model, "ADC0_7": adc_model}), + PinResource("PIO0_2", {"PIO0_2": dio_5v_model, "ADC0_6": adc_model}), + PinResource("PIO0_3", {"PIO0_3": dio_5v_model, "ADC0_5": adc_model}), + PinResource("PIO0_4", {"PIO0_4": dio_5v_model, "ADC0_4": adc_model}), + PinResource("PIO0_5", {"PIO0_5": dio_5v_model, "ADC0_3": adc_model}), + PinResource("PIO0_6", {"PIO0_6": dio_5v_model, "ADC0_2": adc_model}), + PinResource("PIO0_7", {"PIO0_7": dio_5v_model, "ADC0_1": adc_model}), + PinResource("PIO0_8", {"PIO0_8": dio_5v_model, "ADC0_0": adc_model}), + PinResource("PIO0_9", {"PIO0_9": dio_5v_model, "ADC1_1": adc_model}), + PinResource("PIO0_10", {"PIO0_10": dio_5v_model, "ADC1_2": adc_model}), + PinResource("PIO0_11", {"PIO0_11": dio_5v_model, "ADC1_3": adc_model}), + PinResource("PIO0_12", {"PIO0_12": dio_non5v_model, "DAC_OUT": dac_model}), + PinResource("PIO0_13", {"PIO0_13": dio_5v_model, "ADC1_6": adc_model}), + PinResource("PIO0_14", {"PIO0_14": dio_5v_model, "ADC1_7": adc_model}), + PinResource("PIO0_15", {"PIO0_15": dio_5v_model, "ADC1_8": adc_model}), + PinResource("PIO0_16", {"PIO0_16": dio_5v_model, "ADC1_9": adc_model}), + PinResource("PIO0_17", {"PIO0_17": dio_5v_model}), + PinResource("PIO0_18", {"PIO0_18": dio_5v_model}), + PinResource("PIO0_19", {"PIO0_19": dio_5v_model}), + PinResource("PIO0_20", {"PIO0_20": dio_5v_model}), + PinResource("PIO0_21", {"PIO0_21": dio_5v_model}), # also RESET + PinResource("PIO0_22", {"PIO0_22": dio_5v_model}), + PinResource("PIO0_23", {"PIO0_23": dio_5v_model}), + PinResource("PIO0_24", {"PIO0_24": dio_highcurrrent_model}), + PinResource("PIO0_25", {"PIO0_25": dio_5v_model}), + PinResource("PIO0_26", {"PIO0_26": dio_5v_model}), + PinResource("PIO0_27", {"PIO0_27": dio_5v_model}), + PinResource("PIO0_28", {"PIO0_28": dio_5v_model}), + PinResource("PIO0_29", {"PIO0_29": dio_5v_model}), + PinResource("PIO0_30", {"PIO0_30": dio_5v_model, "ADC0_11": adc_model}), + PinResource("PIO0_31", {"PIO0_31": dio_5v_model, "ADC0_9": adc_model}), + PinResource("PIO1_0", {"PIO1_0": dio_5v_model, "ADC0_8": adc_model}), + PinResource("PIO1_1", {"PIO1_1": dio_5v_model, "ADC1_0": adc_model}), + PinResource("PIO1_2", {"PIO1_2": dio_5v_model, "ADC1_4": adc_model}), + PinResource("PIO1_3", {"PIO1_3": dio_5v_model, "ADC1_5": adc_model}), + PinResource("PIO1_4", {"PIO1_4": dio_5v_model, "ADC1_10": adc_model}), + PinResource("PIO1_5", {"PIO1_5": dio_5v_model, "ADC1_11": adc_model}), + PinResource("PIO1_6", {"PIO1_6": dio_5v_model}), + PinResource("PIO1_7", {"PIO1_7": dio_5v_model}), + PinResource("PIO1_8", {"PIO1_8": dio_5v_model}), + PinResource("PIO1_9", {"PIO1_9": dio_5v_model}), + PinResource("PIO1_10", {"PIO1_10": dio_5v_model}), + PinResource("PIO1_11", {"PIO1_11": dio_5v_model}), + PeripheralAnyResource("UART0", uart_model), + PeripheralAnyResource("UART1", uart_model), + PeripheralAnyResource("UART2", uart_model), + PeripheralAnyResource("SPI0", spi_model), + PeripheralAnyResource("SPI1", spi_model), + PeripheralAnyResource("SPI0_P", spi_peripheral_model), # TODO shared resource w/ SPI controller + PeripheralAnyResource("SPI1_P", spi_peripheral_model), # TODO shared resource w/ SPI controller + PeripheralAnyResource("CAN0", CanControllerPort(DigitalBidir.empty())), + PeripheralFixedResource( + "I2C0", I2cController(DigitalBidir.empty()), {"scl": ["PIO0_22"], "sda": ["PIO0_23"]} + ), + PeripheralFixedResource( + "I2C0_T", + I2cTarget(DigitalBidir.empty()), + {"scl": ["PIO0_22"], "sda": ["PIO0_23"]}, # TODO shared resource w/ I2C controller + ), + PeripheralFixedPin("USB", UsbDevicePort(), {"dp": "USB_DP", "dm": "USB_DM"}), + # Figure 49: requires a pull-up on SWDIO and pull-down on SWCLK + PeripheralFixedResource( + "SWD", + SwdTargetPort(DigitalBidir.empty()), + { + "swclk": ["PIO0_19"], + "swdio": ["PIO0_20"], + }, + ), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + self.PACKAGE, + self._make_pinning(), + mfr="NXP", + part=self.PART, + datasheet="https://www.nxp.com/docs/en/data-sheet/LPC15XX.pdf", + ) + self.assign(self.lcsc_part, self.LCSC_PART) + self.assign(self.actual_basic_part, False) class Lpc1549_48_Device(Lpc1549Base_Device): - PACKAGE = 'Package_QFP:LQFP-48_7x7mm_P0.5mm' - PART = 'LPC1549JBD48' - LCSC_PART = 'C71906' - - SYSTEM_PIN_REMAP = { - 'VddA': '16', - 'VssA': '17', - 'VrefP_ADC': '10', - 'VrefP_DAC': '14', - 'VrefN': '11', - 'Vbat': '30', - 'Vdd': ['27', '39', '42'], - 'Vss': ['20', '40', '41'], - 'XTALIN': '26', - 'XTALOUT': '25', - 'RTCXIN': '31', - 'RTCXOUT': '32', - 'RESET': '34', - } - RESOURCE_PIN_REMAP = { - 'PIO0_0': '1', - 'PIO0_1': '2', - 'PIO0_2': '3', - 'PIO0_3': '4', - # 'PIO0_4': '5', # ISP_0 - 'PIO0_5': '6', - 'PIO0_6': '7', - 'PIO0_7': '8', - - 'PIO0_8': '9', - 'PIO0_9': '12', - 'PIO0_10': '15', - 'PIO0_11': '18', - 'PIO0_12': '19', - 'PIO0_13': '21', - 'PIO0_14': '22', - 'PIO0_15': '23', - # 'PIO0_16': '24', # ISP_1 - 'PIO0_17': '28', - - 'PIO0_18': '13', - 'PIO0_19': '29', - 'PIO0_20': '33', - # 'PIO0_21': '34', # RESET - 'PIO0_22': '37', - 'PIO0_23': '38', - 'PIO0_24': '43', - 'PIO0_25': '44', - 'PIO0_26': '45', - - 'PIO0_27': '46', - 'PIO0_28': '47', - 'PIO0_29': '48', - - 'USB_DP': '35', - 'USB_DM': '36', - } + PACKAGE = "Package_QFP:LQFP-48_7x7mm_P0.5mm" + PART = "LPC1549JBD48" + LCSC_PART = "C71906" + + SYSTEM_PIN_REMAP = { + "VddA": "16", + "VssA": "17", + "VrefP_ADC": "10", + "VrefP_DAC": "14", + "VrefN": "11", + "Vbat": "30", + "Vdd": ["27", "39", "42"], + "Vss": ["20", "40", "41"], + "XTALIN": "26", + "XTALOUT": "25", + "RTCXIN": "31", + "RTCXOUT": "32", + "RESET": "34", + } + RESOURCE_PIN_REMAP = { + "PIO0_0": "1", + "PIO0_1": "2", + "PIO0_2": "3", + "PIO0_3": "4", + # 'PIO0_4': '5', # ISP_0 + "PIO0_5": "6", + "PIO0_6": "7", + "PIO0_7": "8", + "PIO0_8": "9", + "PIO0_9": "12", + "PIO0_10": "15", + "PIO0_11": "18", + "PIO0_12": "19", + "PIO0_13": "21", + "PIO0_14": "22", + "PIO0_15": "23", + # 'PIO0_16': '24', # ISP_1 + "PIO0_17": "28", + "PIO0_18": "13", + "PIO0_19": "29", + "PIO0_20": "33", + # 'PIO0_21': '34', # RESET + "PIO0_22": "37", + "PIO0_23": "38", + "PIO0_24": "43", + "PIO0_25": "44", + "PIO0_26": "45", + "PIO0_27": "46", + "PIO0_28": "47", + "PIO0_29": "48", + "USB_DP": "35", + "USB_DM": "36", + } class Lpc1549_64_Device(Lpc1549Base_Device): - PACKAGE = 'Package_QFP:LQFP-64_10x10mm_P0.5mm' - PART = 'LPC1549JBD64' - LCSC_PART = 'C74507' - - SYSTEM_PIN_REMAP = { - 'VddA': '20', - 'VssA': '21', - 'VrefP_ADC': '13', - 'VrefP_DAC': '18', - 'VrefN': '14', - 'Vbat': '41', - 'Vdd': ['22', '37', '52', '57'], - 'Vss': ['26', '27', '55', '56'], - 'XTALIN': '36', - 'XTALOUT': '35', - 'RTCXIN': '42', - 'RTCXOUT': '43', - 'RESET': '45', - } - RESOURCE_PIN_REMAP = { - 'PIO0_0': '2', - 'PIO0_1': '5', - 'PIO0_2': '6', - 'PIO0_3': '7', - 'PIO0_4': '8', - 'PIO0_5': '9', - 'PIO0_6': '10', - 'PIO0_7': '11', - - 'PIO0_8': '12', - 'PIO0_9': '16', - 'PIO0_10': '19', - 'PIO0_11': '23', - 'PIO0_12': '24', - 'PIO0_13': '29', - 'PIO0_14': '30', - 'PIO0_15': '31', - 'PIO0_16': '32', - 'PIO0_17': '39', - - 'PIO0_18': '17', - 'PIO0_19': '40', - 'PIO0_20': '44', - # 'PIO0_21': '45', - 'PIO0_22': '49', - 'PIO0_23': '50', - 'PIO0_24': '58', - 'PIO0_25': '60', - 'PIO0_26': '61', - - 'PIO0_27': '62', - 'PIO0_28': '63', - 'PIO0_29': '64', - 'PIO0_30': '1', - 'PIO0_31': '3', - 'PIO1_0': '4', - 'PIO1_1': '15', - 'PIO1_2': '25', - 'PIO1_3': '28', - 'PIO1_4': '33', - 'PIO1_5': '34', - 'PIO1_6': '46', - 'PIO1_7': '51', - 'PIO1_8': '53', - # 'PIO1_9': '54', # ISP_0 - 'PIO1_10': '59', - # 'PIO1_11': '38', # ISP_1 - - 'USB_DP': '47', - 'USB_DM': '48', - } + PACKAGE = "Package_QFP:LQFP-64_10x10mm_P0.5mm" + PART = "LPC1549JBD64" + LCSC_PART = "C74507" + + SYSTEM_PIN_REMAP = { + "VddA": "20", + "VssA": "21", + "VrefP_ADC": "13", + "VrefP_DAC": "18", + "VrefN": "14", + "Vbat": "41", + "Vdd": ["22", "37", "52", "57"], + "Vss": ["26", "27", "55", "56"], + "XTALIN": "36", + "XTALOUT": "35", + "RTCXIN": "42", + "RTCXOUT": "43", + "RESET": "45", + } + RESOURCE_PIN_REMAP = { + "PIO0_0": "2", + "PIO0_1": "5", + "PIO0_2": "6", + "PIO0_3": "7", + "PIO0_4": "8", + "PIO0_5": "9", + "PIO0_6": "10", + "PIO0_7": "11", + "PIO0_8": "12", + "PIO0_9": "16", + "PIO0_10": "19", + "PIO0_11": "23", + "PIO0_12": "24", + "PIO0_13": "29", + "PIO0_14": "30", + "PIO0_15": "31", + "PIO0_16": "32", + "PIO0_17": "39", + "PIO0_18": "17", + "PIO0_19": "40", + "PIO0_20": "44", + # 'PIO0_21': '45', + "PIO0_22": "49", + "PIO0_23": "50", + "PIO0_24": "58", + "PIO0_25": "60", + "PIO0_26": "61", + "PIO0_27": "62", + "PIO0_28": "63", + "PIO0_29": "64", + "PIO0_30": "1", + "PIO0_31": "3", + "PIO1_0": "4", + "PIO1_1": "15", + "PIO1_2": "25", + "PIO1_3": "28", + "PIO1_4": "33", + "PIO1_5": "34", + "PIO1_6": "46", + "PIO1_7": "51", + "PIO1_8": "53", + # 'PIO1_9': '54', # ISP_0 + "PIO1_10": "59", + # 'PIO1_11': '38', # ISP_1 + "USB_DP": "47", + "USB_DM": "48", + } class Lpc1549SwdPull(InternalSubcircuit, Block): - def __init__(self) -> None: - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - self.swd = self.Port(SwdPullPort(DigitalSource.empty()), [InOut]) + def __init__(self) -> None: + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + self.swd = self.Port(SwdPullPort(DigitalSource.empty()), [InOut]) - @override - def contents(self) -> None: - super().contents() - self.swdio = self.Block(PullupResistor((10, 100) * kOhm(tol=0.05))).connected(self.pwr, self.swd.swdio) - self.swclk = self.Block(PulldownResistor((10, 100) * kOhm(tol=0.05))).connected(self.gnd, self.swd.swclk) + @override + def contents(self) -> None: + super().contents() + self.swdio = self.Block(PullupResistor((10, 100) * kOhm(tol=0.05))).connected(self.pwr, self.swd.swdio) + self.swclk = self.Block(PulldownResistor((10, 100) * kOhm(tol=0.05))).connected(self.gnd, self.swd.swclk) @abstract_block -class Lpc1549Base(Resettable, IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerDac, IoControllerCan, - IoControllerUsb, Microcontroller, IoControllerWithSwdTargetConnector, WithCrystalGenerator, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): - DEVICE: Type[Lpc1549Base_Device] = Lpc1549Base_Device - DEFAULT_CRYSTAL_FREQUENCY = 12*MHertz(tol=0.005) - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.ic: Lpc1549Base_Device - self.generator_param(self.reset.is_connected()) - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.ic = imp.Block(self.DEVICE(pin_assigns=ArrayStringExpr())) # defined in generator to mix in SWO/TDI - self.connect(self.xtal_node, self.ic.xtal) - (self.swd_pull, ), _ = self.chain(self.swd_node, - imp.Block(Lpc1549SwdPull()), - self.ic.swd) - self.connect(self.reset_node, self.ic.reset) - - # one set of 0.1, 0.01uF caps for each Vdd, Vss pin, per reference schematic - self.pwr_cap = ElementDict[DecouplingCapacitor]() - for i in range(3): - self.pwr_cap[i*2] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.pwr_cap[i*2+1] = imp.Block(DecouplingCapacitor(0.01 * uFarad(tol=0.2))) - self.vbat_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - - # one set of 0.1, 10uF caps for each VddA, VssA pin, per reference schematic - self.pwra_cap = ElementDict[DecouplingCapacitor]() - self.pwra_cap[0] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.pwra_cap[1] = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) - - self.vref_cap = ElementDict[DecouplingCapacitor]() - self.vref_cap[0] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.vref_cap[1] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.vref_cap[2] = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.reset) - - @override - def _crystal_required(self) -> bool: # crystal needed for CAN or USB b/c tighter freq tolerance - return len(self.get(self.can.requested())) > 0 or len(self.get(self.usb.requested())) > 0 \ - or super()._crystal_required() +class Lpc1549Base( + Resettable, + IoControllerSpiPeripheral, + IoControllerI2cTarget, + IoControllerDac, + IoControllerCan, + IoControllerUsb, + Microcontroller, + IoControllerWithSwdTargetConnector, + WithCrystalGenerator, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): + DEVICE: Type[Lpc1549Base_Device] = Lpc1549Base_Device + DEFAULT_CRYSTAL_FREQUENCY = 12 * MHertz(tol=0.005) + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.ic: Lpc1549Base_Device + self.generator_param(self.reset.is_connected()) + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.ic = imp.Block(self.DEVICE(pin_assigns=ArrayStringExpr())) # defined in generator to mix in SWO/TDI + self.connect(self.xtal_node, self.ic.xtal) + (self.swd_pull,), _ = self.chain(self.swd_node, imp.Block(Lpc1549SwdPull()), self.ic.swd) + self.connect(self.reset_node, self.ic.reset) + + # one set of 0.1, 0.01uF caps for each Vdd, Vss pin, per reference schematic + self.pwr_cap = ElementDict[DecouplingCapacitor]() + for i in range(3): + self.pwr_cap[i * 2] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.pwr_cap[i * 2 + 1] = imp.Block(DecouplingCapacitor(0.01 * uFarad(tol=0.2))) + self.vbat_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + + # one set of 0.1, 10uF caps for each VddA, VssA pin, per reference schematic + self.pwra_cap = ElementDict[DecouplingCapacitor]() + self.pwra_cap[0] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.pwra_cap[1] = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) + + self.vref_cap = ElementDict[DecouplingCapacitor]() + self.vref_cap[0] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.vref_cap[1] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.vref_cap[2] = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.reset) + + @override + def _crystal_required(self) -> bool: # crystal needed for CAN or USB b/c tighter freq tolerance + return ( + len(self.get(self.can.requested())) > 0 + or len(self.get(self.usb.requested())) > 0 + or super()._crystal_required() + ) class Lpc1549_48(Lpc1549Base): - DEVICE = Lpc1549_48_Device + DEVICE = Lpc1549_48_Device class Lpc1549_64(Lpc1549Base): - DEVICE = Lpc1549_64_Device + DEVICE = Lpc1549_64_Device diff --git a/edg/parts/Microcontroller_Rp2040.py b/edg/parts/Microcontroller_Rp2040.py index 9e1e9d9ef..06f4a7ee6 100644 --- a/edg/parts/Microcontroller_Rp2040.py +++ b/edg/parts/Microcontroller_Rp2040.py @@ -9,472 +9,532 @@ @non_library class Rp2040_Interfaces(IoControllerI2cTarget, IoControllerUsb, BaseIoController): - """Defines base interfaces for ESP32C3 microcontrollers""" + """Defines base interfaces for ESP32C3 microcontrollers""" @non_library class Rp2040_Ios(Rp2040_Interfaces, BaseIoControllerPinmapGenerator): - """IOs definitions independent of infrastructural (e.g. power) pins.""" - RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name - - @abstractmethod - def _vddio(self) -> Port[VoltageLink]: - """Returns VDDIO (can be VoltageSink or VoltageSource).""" - ... - - def _iovdd_model(self) -> VoltageSink: - return VoltageSink( - voltage_limits=(1.62, 3.63)*Volt, # Table 628 - current_draw=(1.2, 4.3)*mAmp + self.io_current_draw.upper() # Table 629 - ) - - def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: - return DigitalBidir.from_supply( # table 4.4 - self.gnd, pwr, - voltage_limit_tolerance=(-0.3, 0.3) * Volt, - current_limits=(-12, 12)*mAmp, # by IOH / IOL modes - input_threshold_abs=(0.8, 2.0)*Volt, # for IOVdd=3.3, TODO other IOVdd ranges - pullup_capable=True, pulldown_capable=True - ) - - @override - def _io_pinmap(self) -> PinMapUtil: - pwr = self._vddio() - dio_usb_model = dio_ft_model = dio_std_model = self._dio_model(pwr) - - adc_model = AnalogSink.from_supply( # Table 626 - self.gnd, pwr, - voltage_limit_tolerance=(0, 0), # ADC input voltage range - signal_limit_tolerance=(0, 0), # ADC input voltage range - impedance=(100, float('inf')) * kOhm - ) - - uart_model = UartPort(DigitalBidir.empty()) - spi_model = SpiController(DigitalBidir.empty()) - i2c_model = I2cController(DigitalBidir.empty()) - i2c_target_model = I2cTarget(DigitalBidir.empty()) - - return PinMapUtil([ - PinResource('GPIO0', {'GPIO0': dio_ft_model}), - PinResource('GPIO1', {'GPIO1': dio_ft_model}), - PinResource('GPIO2', {'GPIO2': dio_ft_model}), - PinResource('GPIO3', {'GPIO3': dio_ft_model}), - PinResource('GPIO4', {'GPIO4': dio_ft_model}), - PinResource('GPIO5', {'GPIO5': dio_ft_model}), - PinResource('GPIO6', {'GPIO6': dio_ft_model}), - PinResource('GPIO7', {'GPIO7': dio_ft_model}), - PinResource('GPIO8', {'GPIO8': dio_ft_model}), - PinResource('GPIO9', {'GPIO9': dio_ft_model}), - PinResource('GPIO10', {'GPIO10': dio_ft_model}), - PinResource('GPIO11', {'GPIO11': dio_ft_model}), - PinResource('GPIO12', {'GPIO12': dio_ft_model}), - PinResource('GPIO13', {'GPIO13': dio_ft_model}), - PinResource('GPIO14', {'GPIO14': dio_ft_model}), - PinResource('GPIO15', {'GPIO15': dio_ft_model}), - PinResource('GPIO16', {'GPIO16': dio_ft_model}), - PinResource('GPIO17', {'GPIO17': dio_ft_model}), - PinResource('GPIO18', {'GPIO18': dio_ft_model}), - PinResource('GPIO19', {'GPIO19': dio_ft_model}), - PinResource('GPIO20', {'GPIO20': dio_ft_model}), - PinResource('GPIO21', {'GPIO21': dio_ft_model}), - - PinResource('GPIO22', {'GPIO22': dio_ft_model}), - PinResource('GPIO23', {'GPIO23': dio_ft_model}), - PinResource('GPIO24', {'GPIO24': dio_ft_model}), - PinResource('GPIO25', {'GPIO25': dio_ft_model}), - - PinResource('GPIO26', {'GPIO26': dio_std_model, 'ADC0': adc_model}), - PinResource('GPIO27', {'GPIO27': dio_std_model, 'ADC1': adc_model}), - PinResource('GPIO28', {'GPIO28': dio_std_model, 'ADC2': adc_model}), - PinResource('GPIO29', {'GPIO29': dio_std_model, 'ADC3': adc_model}), - - # fixed-pin peripherals - PeripheralFixedPin('USB', UsbDevicePort(dio_usb_model), { - 'dm': 'USB_DM', 'dp': 'USB_DP' - }), - - # reassignable peripherals - PeripheralFixedResource('UART0', uart_model, { - 'tx': ['GPIO0', 'GPIO12', 'GPIO16', 'GPIO28'], - 'rx': ['GPIO1', 'GPIO13', 'GPIO17', 'GPIO29'] - }), - PeripheralFixedResource('UART1', uart_model, { - 'tx': ['GPIO4', 'GPIO8', 'GPIO20', 'GPIO24'], - 'rx': ['GPIO5', 'GPIO9', 'GPIO21', 'GPIO25'] - }), - - PeripheralFixedResource('SPI0', spi_model, { - 'miso': ['GPIO0', 'GPIO4', 'GPIO16', 'GPIO20'], # RX - 'sck': ['GPIO2', 'GPIO6', 'GPIO18', 'GPIO22'], - 'mosi': ['GPIO3', 'GPIO7', 'GPIO19', 'GPIO23'] # TX - }), - PeripheralFixedResource('SPI1', spi_model, { - 'miso': ['GPIO8', 'GPIO12', 'GPIO24', 'GPIO28'], # RX - 'sck': ['GPIO10', 'GPIO14', 'GPIO26'], - 'mosi': ['GPIO11', 'GPIO15', 'GPIO27'] # TX - }), - # SPI peripheral omitted, since TX tri-state is not tied to CS and must be controlled in software - PeripheralFixedResource('I2C0', i2c_model, { - 'sda': ['GPIO0', 'GPIO4', 'GPIO8', 'GPIO12', 'GPIO16', 'GPIO20', 'GPIO24', 'GPIO28'], - 'scl': ['GPIO1', 'GPIO5', 'GPIO9', 'GPIO13', 'GPIO17', 'GPIO21', 'GPIO25', 'GPIO20'] - }), - PeripheralFixedResource('I2C1', i2c_model, { - 'sda': ['GPIO2', 'GPIO6', 'GPIO10', 'GPIO14', 'GPIO18', 'GPIO22', 'GPIO24', 'GPIO26'], - 'scl': ['GPIO3', 'GPIO7', 'GPIO11', 'GPIO15', 'GPIO19', 'GPIO23', 'GPIO25', 'GPIO27'] - }), - PeripheralFixedResource('I2C0_T', i2c_target_model, { # TODO shared resource w/ I2C controller - 'sda': ['GPIO0', 'GPIO4', 'GPIO8', 'GPIO12', 'GPIO16', 'GPIO20', 'GPIO24', 'GPIO28'], - 'scl': ['GPIO1', 'GPIO5', 'GPIO9', 'GPIO13', 'GPIO17', 'GPIO21', 'GPIO25', 'GPIO20'] - }), - PeripheralFixedResource('I2C1_T', i2c_target_model, { # TODO shared resource w/ I2C controller - 'sda': ['GPIO2', 'GPIO6', 'GPIO10', 'GPIO14', 'GPIO18', 'GPIO22', 'GPIO24', 'GPIO26'], - 'scl': ['GPIO3', 'GPIO7', 'GPIO11', 'GPIO15', 'GPIO19', 'GPIO23', 'GPIO25', 'GPIO27'] - }), - - PeripheralFixedPin('SWD', SwdTargetPort(dio_std_model), { - 'swdio': 'SWDIO', 'swclk': 'SWCLK', - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) - - -class Rp2040_Device(Rp2040_Ios, BaseIoControllerPinmapGenerator, InternalSubcircuit, - GeneratorBlock, JlcPart, FootprintBlock): - RESOURCE_PIN_REMAP = { - 'GPIO0': '2', - 'GPIO1': '3', - 'GPIO2': '4', - 'GPIO3': '5', - 'GPIO4': '6', - 'GPIO5': '7', - 'GPIO6': '8', - 'GPIO7': '9', - 'GPIO8': '11', - 'GPIO9': '12', - 'GPIO10': '13', - 'GPIO11': '14', - 'GPIO12': '15', - 'GPIO13': '16', - 'GPIO14': '17', - 'GPIO15': '18', - 'GPIO16': '27', - 'GPIO17': '28', - 'GPIO18': '29', - 'GPIO19': '30', - 'GPIO20': '31', - 'GPIO21': '32', - - 'GPIO22': '34', - 'GPIO23': '35', - 'GPIO24': '36', - 'GPIO25': '37', - - 'GPIO26': '38', - 'GPIO27': '39', - 'GPIO28': '40', - 'GPIO29': '41', - - 'USB_DM': '46', - 'USB_DP': '47', - - 'SWDIO': '25', - 'SWCLK': '24', - } - - @override - def _vddio(self) -> Port[VoltageLink]: - return self.iovdd - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.gnd = self.Port(Ground(), [Common]) - self.iovdd = self.Port(self._iovdd_model(), [Power]) - - self.dvdd = self.Port(VoltageSink( # Digital Core - voltage_limits=(0.99, 1.21)*Volt, # Table 628 - current_draw=(0.18, 40)*mAmp, # Table 629 typ Dormant to Figure 171 approx max DVdd - )) - self.vreg_vout = self.Port(VoltageSource( # actually adjustable, section 2.10.3 - voltage_out=1.1*Volt(tol=0.03), # default is 1.1v nominal with 3% variation (Table 192) - current_limits=(0, 100)*mAmp # Table 1, max current - )) - self.vreg_vin = self.Port(VoltageSink( - voltage_limits=(1.62, 3.63)*Volt, # Table 628 - current_draw=self.vreg_vout.is_connected().then_else(self.vreg_vout.link().current_drawn, 0*Amp(tol=0)), - )) - self.usb_vdd = self.Port(VoltageSink( - voltage_limits=RangeExpr(), # depends on if USB is needed - current_draw=(0.2, 2.0)*mAmp, # Table 629 typ BOOTSEL Idle to max BOOTSEL Active - )) - self.adc_avdd = self.Port(VoltageSink( - voltage_limits=(2.97, 3.63)*Volt, # Table 628, performance compromised at <2.97V, lowest 1.62V - # current draw not specified in datasheet - )) - - # Additional ports (on top of IoController) - self.qspi = self.Port(SpiController.empty()) # TODO actually QSPI - self.qspi_cs = self.Port(DigitalBidir.empty()) - self.qspi_sd2 = self.Port(DigitalBidir.empty()) - self.qspi_sd3 = self.Port(DigitalBidir.empty()) - - self.xosc = self.Port(CrystalDriver(frequency_limits=(1, 15)*MHertz, # datasheet 2.15.2.2 - voltage_out=self.iovdd.link().voltage), - optional=True) - - self.swd = self.Port(SwdTargetPort.empty()) - self.run = self.Port(DigitalSink.empty(), optional=True) # internally pulled up - self._io_ports.insert(0, self.swd) - - @override - def contents(self) -> None: - super().contents() - - # Port models - dio_ft_model = dio_std_model = self._dio_model(self.iovdd) - self.qspi.init_from(SpiController(dio_std_model)) - self.qspi_cs.init_from(dio_std_model) - self.qspi_sd2.init_from(dio_std_model) - self.qspi_sd3.init_from(dio_std_model) - self.run.init_from(DigitalSink.from_bidir(dio_ft_model)) - - # Pin/peripheral resource definitions (table 3) - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return { - '51': self.qspi_sd3, - '52': self.qspi.sck, - '53': self.qspi.mosi, # IO0 - '54': self.qspi_sd2, - '55': self.qspi.miso, # IO1 - '56': self.qspi_cs, # IO1 - - '20': self.xosc.xtal_in, - '21': self.xosc.xtal_out, - - '24': self.swd.swclk, - '25': self.swd.swdio, - '26': self.run, - - '19': self.gnd, # TESTEN, connect to gnd - - '1': self.iovdd, - '10': self.iovdd, - '22': self.iovdd, - '33': self.iovdd, - '42': self.iovdd, - '49': self.iovdd, - - '23': self.dvdd, - '50': self.dvdd, - - '44': self.vreg_vin, - '45': self.vreg_vout, - '48': self.usb_vdd, - '43': self.adc_avdd, - '57': self.gnd, # pad + """IOs definitions independent of infrastructural (e.g. power) pins.""" + + RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name + + @abstractmethod + def _vddio(self) -> Port[VoltageLink]: + """Returns VDDIO (can be VoltageSink or VoltageSource).""" + ... + + def _iovdd_model(self) -> VoltageSink: + return VoltageSink( + voltage_limits=(1.62, 3.63) * Volt, # Table 628 + current_draw=(1.2, 4.3) * mAmp + self.io_current_draw.upper(), # Table 629 + ) + + def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: + return DigitalBidir.from_supply( # table 4.4 + self.gnd, + pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_limits=(-12, 12) * mAmp, # by IOH / IOL modes + input_threshold_abs=(0.8, 2.0) * Volt, # for IOVdd=3.3, TODO other IOVdd ranges + pullup_capable=True, + pulldown_capable=True, + ) + + @override + def _io_pinmap(self) -> PinMapUtil: + pwr = self._vddio() + dio_usb_model = dio_ft_model = dio_std_model = self._dio_model(pwr) + + adc_model = AnalogSink.from_supply( # Table 626 + self.gnd, + pwr, + voltage_limit_tolerance=(0, 0), # ADC input voltage range + signal_limit_tolerance=(0, 0), # ADC input voltage range + impedance=(100, float("inf")) * kOhm, + ) + + uart_model = UartPort(DigitalBidir.empty()) + spi_model = SpiController(DigitalBidir.empty()) + i2c_model = I2cController(DigitalBidir.empty()) + i2c_target_model = I2cTarget(DigitalBidir.empty()) + + return PinMapUtil( + [ + PinResource("GPIO0", {"GPIO0": dio_ft_model}), + PinResource("GPIO1", {"GPIO1": dio_ft_model}), + PinResource("GPIO2", {"GPIO2": dio_ft_model}), + PinResource("GPIO3", {"GPIO3": dio_ft_model}), + PinResource("GPIO4", {"GPIO4": dio_ft_model}), + PinResource("GPIO5", {"GPIO5": dio_ft_model}), + PinResource("GPIO6", {"GPIO6": dio_ft_model}), + PinResource("GPIO7", {"GPIO7": dio_ft_model}), + PinResource("GPIO8", {"GPIO8": dio_ft_model}), + PinResource("GPIO9", {"GPIO9": dio_ft_model}), + PinResource("GPIO10", {"GPIO10": dio_ft_model}), + PinResource("GPIO11", {"GPIO11": dio_ft_model}), + PinResource("GPIO12", {"GPIO12": dio_ft_model}), + PinResource("GPIO13", {"GPIO13": dio_ft_model}), + PinResource("GPIO14", {"GPIO14": dio_ft_model}), + PinResource("GPIO15", {"GPIO15": dio_ft_model}), + PinResource("GPIO16", {"GPIO16": dio_ft_model}), + PinResource("GPIO17", {"GPIO17": dio_ft_model}), + PinResource("GPIO18", {"GPIO18": dio_ft_model}), + PinResource("GPIO19", {"GPIO19": dio_ft_model}), + PinResource("GPIO20", {"GPIO20": dio_ft_model}), + PinResource("GPIO21", {"GPIO21": dio_ft_model}), + PinResource("GPIO22", {"GPIO22": dio_ft_model}), + PinResource("GPIO23", {"GPIO23": dio_ft_model}), + PinResource("GPIO24", {"GPIO24": dio_ft_model}), + PinResource("GPIO25", {"GPIO25": dio_ft_model}), + PinResource("GPIO26", {"GPIO26": dio_std_model, "ADC0": adc_model}), + PinResource("GPIO27", {"GPIO27": dio_std_model, "ADC1": adc_model}), + PinResource("GPIO28", {"GPIO28": dio_std_model, "ADC2": adc_model}), + PinResource("GPIO29", {"GPIO29": dio_std_model, "ADC3": adc_model}), + # fixed-pin peripherals + PeripheralFixedPin("USB", UsbDevicePort(dio_usb_model), {"dm": "USB_DM", "dp": "USB_DP"}), + # reassignable peripherals + PeripheralFixedResource( + "UART0", + uart_model, + {"tx": ["GPIO0", "GPIO12", "GPIO16", "GPIO28"], "rx": ["GPIO1", "GPIO13", "GPIO17", "GPIO29"]}, + ), + PeripheralFixedResource( + "UART1", + uart_model, + {"tx": ["GPIO4", "GPIO8", "GPIO20", "GPIO24"], "rx": ["GPIO5", "GPIO9", "GPIO21", "GPIO25"]}, + ), + PeripheralFixedResource( + "SPI0", + spi_model, + { + "miso": ["GPIO0", "GPIO4", "GPIO16", "GPIO20"], # RX + "sck": ["GPIO2", "GPIO6", "GPIO18", "GPIO22"], + "mosi": ["GPIO3", "GPIO7", "GPIO19", "GPIO23"], # TX + }, + ), + PeripheralFixedResource( + "SPI1", + spi_model, + { + "miso": ["GPIO8", "GPIO12", "GPIO24", "GPIO28"], # RX + "sck": ["GPIO10", "GPIO14", "GPIO26"], + "mosi": ["GPIO11", "GPIO15", "GPIO27"], # TX + }, + ), + # SPI peripheral omitted, since TX tri-state is not tied to CS and must be controlled in software + PeripheralFixedResource( + "I2C0", + i2c_model, + { + "sda": ["GPIO0", "GPIO4", "GPIO8", "GPIO12", "GPIO16", "GPIO20", "GPIO24", "GPIO28"], + "scl": ["GPIO1", "GPIO5", "GPIO9", "GPIO13", "GPIO17", "GPIO21", "GPIO25", "GPIO20"], + }, + ), + PeripheralFixedResource( + "I2C1", + i2c_model, + { + "sda": ["GPIO2", "GPIO6", "GPIO10", "GPIO14", "GPIO18", "GPIO22", "GPIO24", "GPIO26"], + "scl": ["GPIO3", "GPIO7", "GPIO11", "GPIO15", "GPIO19", "GPIO23", "GPIO25", "GPIO27"], + }, + ), + PeripheralFixedResource( + "I2C0_T", + i2c_target_model, + { # TODO shared resource w/ I2C controller + "sda": ["GPIO0", "GPIO4", "GPIO8", "GPIO12", "GPIO16", "GPIO20", "GPIO24", "GPIO28"], + "scl": ["GPIO1", "GPIO5", "GPIO9", "GPIO13", "GPIO17", "GPIO21", "GPIO25", "GPIO20"], + }, + ), + PeripheralFixedResource( + "I2C1_T", + i2c_target_model, + { # TODO shared resource w/ I2C controller + "sda": ["GPIO2", "GPIO6", "GPIO10", "GPIO14", "GPIO18", "GPIO22", "GPIO24", "GPIO26"], + "scl": ["GPIO3", "GPIO7", "GPIO11", "GPIO15", "GPIO19", "GPIO23", "GPIO25", "GPIO27"], + }, + ), + PeripheralFixedPin( + "SWD", + SwdTargetPort(dio_std_model), + { + "swdio": "SWDIO", + "swclk": "SWCLK", + }, + ), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) + + +class Rp2040_Device( + Rp2040_Ios, BaseIoControllerPinmapGenerator, InternalSubcircuit, GeneratorBlock, JlcPart, FootprintBlock +): + RESOURCE_PIN_REMAP = { + "GPIO0": "2", + "GPIO1": "3", + "GPIO2": "4", + "GPIO3": "5", + "GPIO4": "6", + "GPIO5": "7", + "GPIO6": "8", + "GPIO7": "9", + "GPIO8": "11", + "GPIO9": "12", + "GPIO10": "13", + "GPIO11": "14", + "GPIO12": "15", + "GPIO13": "16", + "GPIO14": "17", + "GPIO15": "18", + "GPIO16": "27", + "GPIO17": "28", + "GPIO18": "29", + "GPIO19": "30", + "GPIO20": "31", + "GPIO21": "32", + "GPIO22": "34", + "GPIO23": "35", + "GPIO24": "36", + "GPIO25": "37", + "GPIO26": "38", + "GPIO27": "39", + "GPIO28": "40", + "GPIO29": "41", + "USB_DM": "46", + "USB_DP": "47", + "SWDIO": "25", + "SWCLK": "24", } - @override - def generate(self) -> None: - super().generate() + @override + def _vddio(self) -> Port[VoltageLink]: + return self.iovdd + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.gnd = self.Port(Ground(), [Common]) + self.iovdd = self.Port(self._iovdd_model(), [Power]) + + self.dvdd = self.Port( + VoltageSink( # Digital Core + voltage_limits=(0.99, 1.21) * Volt, # Table 628 + current_draw=(0.18, 40) * mAmp, # Table 629 typ Dormant to Figure 171 approx max DVdd + ) + ) + self.vreg_vout = self.Port( + VoltageSource( # actually adjustable, section 2.10.3 + voltage_out=1.1 * Volt(tol=0.03), # default is 1.1v nominal with 3% variation (Table 192) + current_limits=(0, 100) * mAmp, # Table 1, max current + ) + ) + self.vreg_vin = self.Port( + VoltageSink( + voltage_limits=(1.62, 3.63) * Volt, # Table 628 + current_draw=self.vreg_vout.is_connected().then_else( + self.vreg_vout.link().current_drawn, 0 * Amp(tol=0) + ), + ) + ) + self.usb_vdd = self.Port( + VoltageSink( + voltage_limits=RangeExpr(), # depends on if USB is needed + current_draw=(0.2, 2.0) * mAmp, # Table 629 typ BOOTSEL Idle to max BOOTSEL Active + ) + ) + self.adc_avdd = self.Port( + VoltageSink( + voltage_limits=(2.97, 3.63) * Volt, # Table 628, performance compromised at <2.97V, lowest 1.62V + # current draw not specified in datasheet + ) + ) + + # Additional ports (on top of IoController) + self.qspi = self.Port(SpiController.empty()) # TODO actually QSPI + self.qspi_cs = self.Port(DigitalBidir.empty()) + self.qspi_sd2 = self.Port(DigitalBidir.empty()) + self.qspi_sd3 = self.Port(DigitalBidir.empty()) + + self.xosc = self.Port( + CrystalDriver( + frequency_limits=(1, 15) * MHertz, voltage_out=self.iovdd.link().voltage # datasheet 2.15.2.2 + ), + optional=True, + ) + + self.swd = self.Port(SwdTargetPort.empty()) + self.run = self.Port(DigitalSink.empty(), optional=True) # internally pulled up + self._io_ports.insert(0, self.swd) + + @override + def contents(self) -> None: + super().contents() + + # Port models + dio_ft_model = dio_std_model = self._dio_model(self.iovdd) + self.qspi.init_from(SpiController(dio_std_model)) + self.qspi_cs.init_from(dio_std_model) + self.qspi_sd2.init_from(dio_std_model) + self.qspi_sd3.init_from(dio_std_model) + self.run.init_from(DigitalSink.from_bidir(dio_ft_model)) + + # Pin/peripheral resource definitions (table 3) + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return { + "51": self.qspi_sd3, + "52": self.qspi.sck, + "53": self.qspi.mosi, # IO0 + "54": self.qspi_sd2, + "55": self.qspi.miso, # IO1 + "56": self.qspi_cs, # IO1 + "20": self.xosc.xtal_in, + "21": self.xosc.xtal_out, + "24": self.swd.swclk, + "25": self.swd.swdio, + "26": self.run, + "19": self.gnd, # TESTEN, connect to gnd + "1": self.iovdd, + "10": self.iovdd, + "22": self.iovdd, + "33": self.iovdd, + "42": self.iovdd, + "49": self.iovdd, + "23": self.dvdd, + "50": self.dvdd, + "44": self.vreg_vin, + "45": self.vreg_vout, + "48": self.usb_vdd, + "43": self.adc_avdd, + "57": self.gnd, # pad + } + + @override + def generate(self) -> None: + super().generate() + + if not self.get(self.usb.requested()): # Table 628, VDD_USB can be lower if USB not used (section 2.9.4) + self.assign(self.usb_vdd.voltage_limits, (1.62, 3.63) * Volt) + else: + self.assign(self.usb_vdd.voltage_limits, (3.135, 3.63) * Volt) + + self.footprint( + "U", + "Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP3.2x3.2mm", + self._make_pinning(), + mfr="Raspberry Pi", + part="RP2040", + datasheet="https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf", + ) + self.assign(self.lcsc_part, "C2040") + self.assign(self.actual_basic_part, False) - if not self.get(self.usb.requested()): # Table 628, VDD_USB can be lower if USB not used (section 2.9.4) - self.assign(self.usb_vdd.voltage_limits, (1.62, 3.63)*Volt) - else: - self.assign(self.usb_vdd.voltage_limits, (3.135, 3.63)*Volt) - self.footprint( - 'U', 'Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP3.2x3.2mm', - self._make_pinning(), - mfr='Raspberry Pi', part='RP2040', - datasheet='https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf' - ) - self.assign(self.lcsc_part, 'C2040') - self.assign(self.actual_basic_part, False) +class Rp2040Usb(InternalSubcircuit, Block): + """Supporting passives for USB for RP2040""" + + def __init__(self) -> None: + super().__init__() + self.usb_rp = self.Port(UsbHostPort.empty(), [Input]) + self.usb = self.Port(UsbDevicePort.empty(), [Output]) + @override + def contents(self) -> None: + super().contents() -class Rp2040Usb(InternalSubcircuit, Block): - """Supporting passives for USB for RP2040""" - def __init__(self) -> None: - super().__init__() - self.usb_rp = self.Port(UsbHostPort.empty(), [Input]) - self.usb = self.Port(UsbDevicePort.empty(), [Output]) - - @override - def contents(self) -> None: - super().contents() - - self.dp_res = self.Block(Resistor(27*Ohm(tol=0.05))) - self.dm_res = self.Block(Resistor(27*Ohm(tol=0.05))) - - self.connect(self.usb_rp.dm, self.dm_res.a.adapt_to(DigitalBidir())) # internal ports are ideal - self.connect(self.usb.dm, self.dm_res.b.adapt_to( - UsbBitBang.digital_external_from_link(self.usb_rp.dm))) - - self.connect(self.usb_rp.dp, self.dp_res.a.adapt_to(DigitalBidir())) - self.connect(self.usb.dp, self.dp_res.b.adapt_to( - UsbBitBang.digital_external_from_link(self.usb_rp.dp))) - - -class Rp2040(Resettable, Rp2040_Interfaces, Microcontroller, IoControllerWithSwdTargetConnector, - WithCrystalGenerator, IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): - DEFAULT_CRYSTAL_FREQUENCY = 12*MHertz(tol=0.005) - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.ic: Rp2040_Device - self.generator_param(self.reset.is_connected()) - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - # https://datasheets.raspberrypi.com/rp2040/hardware-design-with-rp2040.pdf - self.ic = imp.Block(Rp2040_Device(pin_assigns=ArrayStringExpr())) - self.connect(self.xtal_node, self.ic.xosc) - self.connect(self.swd_node, self.ic.swd) - self.connect(self.reset_node, self.ic.run) - - self.iovdd_cap = ElementDict[DecouplingCapacitor]() - for i in range(6): # one per IOVdd, combining USBVdd and IOVdd pin 49 per the example - self.iovdd_cap[i] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.avdd_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - - self.vreg_in_cap = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))) - - self.mem = imp.Block(SpiMemory(Range.all())) - self.connect(self.ic.qspi, self.mem.spi) - self.connect(self.ic.qspi_cs, self.mem.cs) - mem_qspi = self.mem.with_mixin(SpiMemoryQspi()) - self.connect(self.ic.qspi_sd2, mem_qspi.io2) - self.connect(self.ic.qspi_sd3, mem_qspi.io3) - - self.connect(self.pwr, self.ic.vreg_vin, self.ic.adc_avdd, self.ic.usb_vdd) - self.connect(self.ic.vreg_vout, self.ic.dvdd) - - self.dvdd_cap = ElementDict[DecouplingCapacitor]() - for i in range(2): - self.dvdd_cap[i] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.dvdd) - - self.vreg_out_cap = self.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.dvdd) - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.run) - - ExportType = TypeVar('ExportType', bound=Port) - @override - def _make_export_vector(self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, - assign: Optional[str]) -> Optional[str]: - if isinstance(self_io, UsbDevicePort): # assumed at most one USB port generates - inner_io = inner_vector.request(name) - (self.usb_res, ), self.usb_chain = self.chain(inner_io, self.Block(Rp2040Usb()), self_io) - return assign - return super()._make_export_vector(self_io, inner_vector, name, assign) - - @override - def _crystal_required(self) -> bool: # crystal needed for USB b/c tighter freq tolerance - return len(self.get(self.usb.requested())) > 0 or super()._crystal_required() - - -class Xiao_Rp2040(IoControllerUsbOut, IoControllerPowerOut, IoControllerVin, Rp2040_Ios, IoController, GeneratorBlock, - FootprintBlock): - """RP2040 development board, a tiny development (21x17.5mm) daughterboard. - Has an onboard USB connector, so this can also source power. - - Limited pins (only 11 for IOs, of which 6 are usable as the other 5 have boot requirements). - - Requires Seeed Studio's KiCad library for the footprint: https://github.com/Seeed-Studio/OPL_Kicad_Library - The 'Seeed Studio XIAO Series Library' must have been added as a footprint library of the same name. - - Pinning data: https://www.seeedstudio.com/blog/wp-content/uploads/2022/08/Seeed-Studio-XIAO-Series-Package-and-PCB-Design.pdf - Internal data: https://files.seeedstudio.com/wiki/XIAO-RP2040/res/Seeed-Studio-XIAO-RP2040-v1.3.pdf - """ - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'VDD': '12', - 'GND': '13', - 'VUSB': '14', - } - RESOURCE_PIN_REMAP = { - 'GPIO26': '1', - 'GPIO27': '2', - 'GPIO28': '3', - 'GPIO29': '4', - 'GPIO6': '5', - 'GPIO7': '6', - 'GPIO0': '7', - - 'GPIO1': '8', - 'GPIO2': '9', - 'GPIO4': '10', - 'GPIO3': '11', - } - - @override - def _vddio(self) -> Port[VoltageLink]: - if self.get(self.pwr.is_connected()): # board sinks power - return self.pwr - else: - return self.pwr_out - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - if self.get(self.pwr.is_connected()): # board sinks power - return VariantPinRemapper({ - 'VDD': self.pwr, - 'GND': self.gnd, - }).remap(self.SYSTEM_PIN_REMAP) - else: # board sources power (default) - return VariantPinRemapper({ - 'VDD': self.pwr_out, - 'GND': self.gnd, - 'VUSB': self.vusb_out, - }).remap(self.SYSTEM_PIN_REMAP) - - @override - def contents(self) -> None: - super().contents() - - self.gnd.init_from(Ground()) - self.pwr.init_from(self._iovdd_model()) - - self.pwr_vin.init_from(VoltageSink( # based on RS3236-3.3 - voltage_limits=(3.3*1.025 + 0.55, 7.5)*Volt, # output * tolerance + dropout @ 300mA - current_draw=RangeExpr() - )) - self.vusb_out.init_from(VoltageSource( - voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - self.require(~self.pwr_vin.is_connected() | ~self.vusb_out.is_connected(), "cannot use both VUsb out and VUsb in") - self.require((self.pwr_vin.is_connected() | self.vusb_out.is_connected()).implies(~self.pwr.is_connected()), - "cannot use 3.3v input if VUsb used") - - self.pwr_out.init_from(VoltageSource( - voltage_out=3.3*Volt(tol=0.05), # tolerance is a guess - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - self.require(~self.pwr_out.is_connected() | ~self.pwr.is_connected(), "cannot use both 3.3v out and 3.3v in") - self.assign(self.pwr_vin.current_draw, self.pwr_out.is_connected().then_else( # prop output current draw - self.pwr_out.link().current_drawn, (0, 0)*Amp - )) - - self.generator_param(self.pwr.is_connected()) - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', 'Seeed Studio XIAO Series Library:XIAO-RP2040-SMD', - self._make_pinning(), - mfr='', part='XIAO RP2040', - datasheet='https://www.seeedstudio.com/XIAO-RP2040-v1-0-p-5026.html' - ) + self.dp_res = self.Block(Resistor(27 * Ohm(tol=0.05))) + self.dm_res = self.Block(Resistor(27 * Ohm(tol=0.05))) + + self.connect(self.usb_rp.dm, self.dm_res.a.adapt_to(DigitalBidir())) # internal ports are ideal + self.connect(self.usb.dm, self.dm_res.b.adapt_to(UsbBitBang.digital_external_from_link(self.usb_rp.dm))) + + self.connect(self.usb_rp.dp, self.dp_res.a.adapt_to(DigitalBidir())) + self.connect(self.usb.dp, self.dp_res.b.adapt_to(UsbBitBang.digital_external_from_link(self.usb_rp.dp))) + + +class Rp2040( + Resettable, + Rp2040_Interfaces, + Microcontroller, + IoControllerWithSwdTargetConnector, + WithCrystalGenerator, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): + DEFAULT_CRYSTAL_FREQUENCY = 12 * MHertz(tol=0.005) + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.ic: Rp2040_Device + self.generator_param(self.reset.is_connected()) + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + # https://datasheets.raspberrypi.com/rp2040/hardware-design-with-rp2040.pdf + self.ic = imp.Block(Rp2040_Device(pin_assigns=ArrayStringExpr())) + self.connect(self.xtal_node, self.ic.xosc) + self.connect(self.swd_node, self.ic.swd) + self.connect(self.reset_node, self.ic.run) + + self.iovdd_cap = ElementDict[DecouplingCapacitor]() + for i in range(6): # one per IOVdd, combining USBVdd and IOVdd pin 49 per the example + self.iovdd_cap[i] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.avdd_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + + self.vreg_in_cap = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))) + + self.mem = imp.Block(SpiMemory(Range.all())) + self.connect(self.ic.qspi, self.mem.spi) + self.connect(self.ic.qspi_cs, self.mem.cs) + mem_qspi = self.mem.with_mixin(SpiMemoryQspi()) + self.connect(self.ic.qspi_sd2, mem_qspi.io2) + self.connect(self.ic.qspi_sd3, mem_qspi.io3) + + self.connect(self.pwr, self.ic.vreg_vin, self.ic.adc_avdd, self.ic.usb_vdd) + self.connect(self.ic.vreg_vout, self.ic.dvdd) + + self.dvdd_cap = ElementDict[DecouplingCapacitor]() + for i in range(2): + self.dvdd_cap[i] = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.dvdd) + + self.vreg_out_cap = self.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.dvdd) + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.run) + + ExportType = TypeVar("ExportType", bound=Port) + + @override + def _make_export_vector( + self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, assign: Optional[str] + ) -> Optional[str]: + if isinstance(self_io, UsbDevicePort): # assumed at most one USB port generates + inner_io = inner_vector.request(name) + (self.usb_res,), self.usb_chain = self.chain(inner_io, self.Block(Rp2040Usb()), self_io) + return assign + return super()._make_export_vector(self_io, inner_vector, name, assign) + + @override + def _crystal_required(self) -> bool: # crystal needed for USB b/c tighter freq tolerance + return len(self.get(self.usb.requested())) > 0 or super()._crystal_required() + + +class Xiao_Rp2040( + IoControllerUsbOut, IoControllerPowerOut, IoControllerVin, Rp2040_Ios, IoController, GeneratorBlock, FootprintBlock +): + """RP2040 development board, a tiny development (21x17.5mm) daughterboard. + Has an onboard USB connector, so this can also source power. + + Limited pins (only 11 for IOs, of which 6 are usable as the other 5 have boot requirements). + + Requires Seeed Studio's KiCad library for the footprint: https://github.com/Seeed-Studio/OPL_Kicad_Library + The 'Seeed Studio XIAO Series Library' must have been added as a footprint library of the same name. + + Pinning data: https://www.seeedstudio.com/blog/wp-content/uploads/2022/08/Seeed-Studio-XIAO-Series-Package-and-PCB-Design.pdf + Internal data: https://files.seeedstudio.com/wiki/XIAO-RP2040/res/Seeed-Studio-XIAO-RP2040-v1.3.pdf + """ + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "VDD": "12", + "GND": "13", + "VUSB": "14", + } + RESOURCE_PIN_REMAP = { + "GPIO26": "1", + "GPIO27": "2", + "GPIO28": "3", + "GPIO29": "4", + "GPIO6": "5", + "GPIO7": "6", + "GPIO0": "7", + "GPIO1": "8", + "GPIO2": "9", + "GPIO4": "10", + "GPIO3": "11", + } + + @override + def _vddio(self) -> Port[VoltageLink]: + if self.get(self.pwr.is_connected()): # board sinks power + return self.pwr + else: + return self.pwr_out + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + if self.get(self.pwr.is_connected()): # board sinks power + return VariantPinRemapper( + { + "VDD": self.pwr, + "GND": self.gnd, + } + ).remap(self.SYSTEM_PIN_REMAP) + else: # board sources power (default) + return VariantPinRemapper( + { + "VDD": self.pwr_out, + "GND": self.gnd, + "VUSB": self.vusb_out, + } + ).remap(self.SYSTEM_PIN_REMAP) + + @override + def contents(self) -> None: + super().contents() + + self.gnd.init_from(Ground()) + self.pwr.init_from(self._iovdd_model()) + + self.pwr_vin.init_from( + VoltageSink( # based on RS3236-3.3 + voltage_limits=(3.3 * 1.025 + 0.55, 7.5) * Volt, # output * tolerance + dropout @ 300mA + current_draw=RangeExpr(), + ) + ) + self.vusb_out.init_from( + VoltageSource(voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, current_limits=UsbConnector.USB2_CURRENT_LIMITS) + ) + self.require( + ~self.pwr_vin.is_connected() | ~self.vusb_out.is_connected(), "cannot use both VUsb out and VUsb in" + ) + self.require( + (self.pwr_vin.is_connected() | self.vusb_out.is_connected()).implies(~self.pwr.is_connected()), + "cannot use 3.3v input if VUsb used", + ) + + self.pwr_out.init_from( + VoltageSource( + voltage_out=3.3 * Volt(tol=0.05), # tolerance is a guess + current_limits=UsbConnector.USB2_CURRENT_LIMITS, + ) + ) + self.require(~self.pwr_out.is_connected() | ~self.pwr.is_connected(), "cannot use both 3.3v out and 3.3v in") + self.assign( + self.pwr_vin.current_draw, + self.pwr_out.is_connected().then_else( # prop output current draw + self.pwr_out.link().current_drawn, (0, 0) * Amp + ), + ) + + self.generator_param(self.pwr.is_connected()) + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + "Seeed Studio XIAO Series Library:XIAO-RP2040-SMD", + self._make_pinning(), + mfr="", + part="XIAO RP2040", + datasheet="https://www.seeedstudio.com/XIAO-RP2040-v1-0-p-5026.html", + ) diff --git a/edg/parts/Microcontroller_Stm32f103.py b/edg/parts/Microcontroller_Stm32f103.py index f8bef26a5..0b111e724 100644 --- a/edg/parts/Microcontroller_Stm32f103.py +++ b/edg/parts/Microcontroller_Stm32f103.py @@ -7,325 +7,355 @@ @abstract_block -class Stm32f103Base_Device(IoControllerI2cTarget, IoControllerCan, IoControllerUsb, InternalSubcircuit, BaseIoControllerPinmapGenerator, - GeneratorBlock, JlcPart, FootprintBlock): - PACKAGE: str # package name for footprint(...) - PART: str # part name for footprint(...) - LCSC_PART: str - LCSC_BASIC_PART: bool - - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) - RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - # Additional ports (on top of BaseIoController) - self.pwr = self.Port(VoltageSink( - voltage_limits=(3.0, 3.6)*Volt, # TODO relaxed range down to 2.0 if ADC not used, or 2.4 if USB not used - current_draw=(0, 50.3)*mAmp + self.io_current_draw.upper() # Table 13 - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - - self.nrst = self.Port(DigitalSink.from_supply( - self.gnd, self.pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # Table 5.3.1, general operating conditions TODO: FT IO, BOOT0 IO - input_threshold_abs=(0.8, 2)*Volt, - pullup_capable=True - ), optional=True) # note, internal pull-up resistor, 30-50 kOhm by Table 35 - - # TODO need to pass through to pin mapper - # self.osc32 = self.Port(CrystalDriver(frequency_limits=32.768*kHertz(tol=0), # TODO actual tolerances - # voltage_out=self.pwr.link().voltage), - # optional=True) # TODO other specs from Table 23 - self.osc = self.Port(CrystalDriver(frequency_limits=(4, 16)*MHertz, - voltage_out=self.pwr.link().voltage), - optional=True) # Table 22 - - self.swd = self.Port(SwdTargetPort.empty()) - self._io_ports.insert(0, self.swd) - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ # Pin/peripheral resource definitions (table 3) - 'Vbat': self.pwr, - 'VddA': self.pwr, - 'VssA': self.gnd, - 'Vss': self.gnd, - 'Vdd': self.pwr, - 'BOOT0': self.gnd, - 'OSC_IN': self.osc.xtal_in, # TODO remappable to PD0 - 'OSC_OUT': self.osc.xtal_out, # TODO remappable to PD1 - 'NRST': self.nrst, - }).remap(self.SYSTEM_PIN_REMAP) - - @override - def _io_pinmap(self) -> PinMapUtil: - # Port models - dio_ft_model = DigitalBidir.from_supply( - self.gnd, self.pwr, - voltage_limit_abs=(-0.3, 5.2) * Volt, # Table 5.3.1, general operating conditions, TODO relaxed for Vdd>2v - current_limits=(-20, 20)*mAmp, # Section 5.3.13 Output driving current, TODO loose with relaxed VOL/VOH - input_threshold_factor=(0.35, 0.65), # TODO relaxed (but more complex) bounds available - pullup_capable=True, pulldown_capable=True - ) - dio_std_model = DigitalBidir.from_supply( - self.gnd, self.pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # Table 5.3.1, general operating conditions - current_limits=(-20, 20)*mAmp, # Section 5.3.13 Output driving current, TODO loose with relaxed VOL/VOH - input_threshold_factor=(0.35, 0.65), # TODO relaxed (but more complex) bounds available - pullup_capable=True, pulldown_capable=True, - ) - dio_pc_13_14_15_model = DigitalBidir.from_supply( - self.gnd, self.pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # Table 5.3.1, general operating conditions - current_limits=(-3, 3)*mAmp, # Section 5.3.13 Output driving current - input_threshold_factor=(0.35, 0.65), # TODO relaxed (but more complex) bounds available - pullup_capable=True, pulldown_capable=True, - ) - - adc_model = AnalogSink.from_supply( - self.gnd, self.pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # general operating conditions, IO input voltage - signal_limit_tolerance=(0, 0), # conversion voltage range, 0 to Vref+ (assumed VddA) - impedance=(100, float('inf')) * kOhm - ) - - uart_model = UartPort(DigitalBidir.empty()) - spi_model = SpiController(DigitalBidir.empty()) - # TODO SPI peripherals, which have fixed-pin CS lines - i2c_model = I2cController(DigitalBidir.empty()) - i2c_target_model = I2cTarget(DigitalBidir.empty()) - - return PinMapUtil([ # Table 5, partial table for 48-pin only - PinResource('PA0', {'PA0': dio_std_model, 'ADC12_IN0': adc_model}), - PinResource('PA1', {'PA1': dio_std_model, 'ADC12_IN1': adc_model}), - PinResource('PA2', {'PA2': dio_std_model, 'ADC12_IN2': adc_model}), - PinResource('PA3', {'PA3': dio_std_model, 'ADC12_IN3': adc_model}), - PinResource('PA4', {'PA4': dio_std_model, 'ADC12_IN4': adc_model}), - PinResource('PA5', {'PA5': dio_std_model, 'ADC12_IN5': adc_model}), - PinResource('PA6', {'PA6': dio_std_model, 'ADC12_IN6': adc_model}), - PinResource('PA7', {'PA7': dio_std_model, 'ADC12_IN7': adc_model}), - PinResource('PB0', {'PB0': dio_std_model, 'ADC12_IN8': adc_model}), - PinResource('PB1', {'PB1': dio_std_model, 'ADC12_IN9': adc_model}), - - PinResource('PB2', {'PB2': dio_ft_model}), # BOOT1 - PinResource('PB10', {'PB10': dio_ft_model}), - PinResource('PB11', {'PB11': dio_ft_model}), - PinResource('PB12', {'PB12': dio_ft_model}), - PinResource('PB13', {'PB13': dio_ft_model}), - PinResource('PB14', {'PB14': dio_ft_model}), - PinResource('PB15', {'PB15': dio_ft_model}), - - PinResource('PA8', {'PA8': dio_ft_model}), - PinResource('PA9', {'PA9': dio_ft_model}), - PinResource('PA10', {'PA10': dio_ft_model}), - PinResource('PA11', {'PA11': dio_ft_model}), - PinResource('PA12', {'PA12': dio_ft_model}), - # PinResource('PA13', {'PA13': dio_ft_model}), # forced SWDIO default is JTMS/SWDIO - - # PinResource('PA14', {'PA14': dio_ft_model}), # forced SWCLK, default is JTCK/SWCLK - PinResource('PA15', {'PA15': dio_ft_model}), # default is JTDI - PinResource('PB3', {'PB3': dio_ft_model}), # SWO, default is JTDO - PinResource('PB4', {'PB4': dio_ft_model}), # default is JNTRST - PinResource('PB5', {'PB5': dio_std_model}), - PinResource('PB6', {'PB6': dio_ft_model}), - PinResource('PB7', {'PB7': dio_ft_model}), - PinResource('PB8', {'PB8': dio_ft_model}), - PinResource('PB9', {'PB9': dio_ft_model}), - - # PinResource('NRST', {'NRST': dio_std_model}), # non-mappable to IO! - - # de-prioritize these for auto-assignment since they're low-current - PinResource('PC13', {'PC13': dio_pc_13_14_15_model}), - PinResource('PC14', {'PC14': dio_pc_13_14_15_model, 'OSC32_IN': Passive()}), - PinResource('PC15', {'PC15': dio_pc_13_14_15_model, 'OSC32_OUT': Passive()}), - - PeripheralFixedResource('USART2', uart_model, { - 'tx': ['PA2', 'PD5'], 'rx': ['PA3', 'PD6'] - }), - PeripheralFixedResource('SPI1', spi_model, { - 'sck': ['PA5', 'PB3'], 'miso': ['PA6', 'PB4'], 'mosi': ['PA7', 'PB5'] - }), - PeripheralFixedResource('USART3', uart_model, { - 'tx': ['PB10', 'PD8', 'PC10'], 'rx': ['PB11', 'PD9', 'PC11'] - }), - PeripheralFixedResource('I2C2', i2c_model, { - 'scl': ['PB10'], 'sda': ['PB11'] - }), - PeripheralFixedResource('I2C2_T', i2c_target_model, { # TODO shared resource w/ I2C controller - 'scl': ['PB10'], 'sda': ['PB11'] - }), - PeripheralFixedResource('SPI2', spi_model, { - 'sck': ['PB13'], 'miso': ['PB14'], 'mosi': ['PB15'] - }), - PeripheralFixedResource('USART1', uart_model, { - 'tx': ['PA9', 'PB6'], 'rx': ['PA10', 'PB7'] - }), - PeripheralFixedResource('CAN', CanControllerPort(DigitalBidir.empty()), { - 'txd': ['PA12', 'PD1', 'PB9'], 'rxd': ['PA11', 'PD0', 'PB8'] - }), - PeripheralFixedResource('USB', UsbDevicePort(DigitalBidir.empty()), { - 'dm': ['PA11'], 'dp': ['PA12'] - }), - PeripheralFixedPin('SWD', SwdTargetPort(dio_std_model), { # TODO most are FT pins - 'swdio': 'PA13', 'swclk': 'PA14', # note: SWO is PB3 - }), - PeripheralFixedResource('I2C1', i2c_model, { - 'scl': ['PB6', 'PB8'], 'sda': ['PB7', 'PB9'] - }), - PeripheralFixedResource('I2C1_T', i2c_target_model, { # TODO shared resource w/ I2C controller - 'scl': ['PB6', 'PB8'], 'sda': ['PB7', 'PB9'] - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', self.PACKAGE, - self._make_pinning(), - mfr='STMicroelectronics', part=self.PART, - datasheet='https://www.st.com/resource/en/datasheet/stm32f103c8.pdf' - ) - self.assign(self.lcsc_part, self.LCSC_PART) - self.assign(self.actual_basic_part, self.LCSC_BASIC_PART) +class Stm32f103Base_Device( + IoControllerI2cTarget, + IoControllerCan, + IoControllerUsb, + InternalSubcircuit, + BaseIoControllerPinmapGenerator, + GeneratorBlock, + JlcPart, + FootprintBlock, +): + PACKAGE: str # package name for footprint(...) + PART: str # part name for footprint(...) + LCSC_PART: str + LCSC_BASIC_PART: bool + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) + RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + # Additional ports (on top of BaseIoController) + self.pwr = self.Port( + VoltageSink( + voltage_limits=(3.0, 3.6) + * Volt, # TODO relaxed range down to 2.0 if ADC not used, or 2.4 if USB not used + current_draw=(0, 50.3) * mAmp + self.io_current_draw.upper(), # Table 13 + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + + self.nrst = self.Port( + DigitalSink.from_supply( + self.gnd, + self.pwr, + voltage_limit_tolerance=(-0.3, 0.3) + * Volt, # Table 5.3.1, general operating conditions TODO: FT IO, BOOT0 IO + input_threshold_abs=(0.8, 2) * Volt, + pullup_capable=True, + ), + optional=True, + ) # note, internal pull-up resistor, 30-50 kOhm by Table 35 + + # TODO need to pass through to pin mapper + # self.osc32 = self.Port(CrystalDriver(frequency_limits=32.768*kHertz(tol=0), # TODO actual tolerances + # voltage_out=self.pwr.link().voltage), + # optional=True) # TODO other specs from Table 23 + self.osc = self.Port( + CrystalDriver(frequency_limits=(4, 16) * MHertz, voltage_out=self.pwr.link().voltage), optional=True + ) # Table 22 + + self.swd = self.Port(SwdTargetPort.empty()) + self._io_ports.insert(0, self.swd) + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return VariantPinRemapper( + { # Pin/peripheral resource definitions (table 3) + "Vbat": self.pwr, + "VddA": self.pwr, + "VssA": self.gnd, + "Vss": self.gnd, + "Vdd": self.pwr, + "BOOT0": self.gnd, + "OSC_IN": self.osc.xtal_in, # TODO remappable to PD0 + "OSC_OUT": self.osc.xtal_out, # TODO remappable to PD1 + "NRST": self.nrst, + } + ).remap(self.SYSTEM_PIN_REMAP) + + @override + def _io_pinmap(self) -> PinMapUtil: + # Port models + dio_ft_model = DigitalBidir.from_supply( + self.gnd, + self.pwr, + voltage_limit_abs=(-0.3, 5.2) * Volt, # Table 5.3.1, general operating conditions, TODO relaxed for Vdd>2v + current_limits=(-20, 20) * mAmp, # Section 5.3.13 Output driving current, TODO loose with relaxed VOL/VOH + input_threshold_factor=(0.35, 0.65), # TODO relaxed (but more complex) bounds available + pullup_capable=True, + pulldown_capable=True, + ) + dio_std_model = DigitalBidir.from_supply( + self.gnd, + self.pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, # Table 5.3.1, general operating conditions + current_limits=(-20, 20) * mAmp, # Section 5.3.13 Output driving current, TODO loose with relaxed VOL/VOH + input_threshold_factor=(0.35, 0.65), # TODO relaxed (but more complex) bounds available + pullup_capable=True, + pulldown_capable=True, + ) + dio_pc_13_14_15_model = DigitalBidir.from_supply( + self.gnd, + self.pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, # Table 5.3.1, general operating conditions + current_limits=(-3, 3) * mAmp, # Section 5.3.13 Output driving current + input_threshold_factor=(0.35, 0.65), # TODO relaxed (but more complex) bounds available + pullup_capable=True, + pulldown_capable=True, + ) + + adc_model = AnalogSink.from_supply( + self.gnd, + self.pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, # general operating conditions, IO input voltage + signal_limit_tolerance=(0, 0), # conversion voltage range, 0 to Vref+ (assumed VddA) + impedance=(100, float("inf")) * kOhm, + ) + + uart_model = UartPort(DigitalBidir.empty()) + spi_model = SpiController(DigitalBidir.empty()) + # TODO SPI peripherals, which have fixed-pin CS lines + i2c_model = I2cController(DigitalBidir.empty()) + i2c_target_model = I2cTarget(DigitalBidir.empty()) + + return PinMapUtil( + [ # Table 5, partial table for 48-pin only + PinResource("PA0", {"PA0": dio_std_model, "ADC12_IN0": adc_model}), + PinResource("PA1", {"PA1": dio_std_model, "ADC12_IN1": adc_model}), + PinResource("PA2", {"PA2": dio_std_model, "ADC12_IN2": adc_model}), + PinResource("PA3", {"PA3": dio_std_model, "ADC12_IN3": adc_model}), + PinResource("PA4", {"PA4": dio_std_model, "ADC12_IN4": adc_model}), + PinResource("PA5", {"PA5": dio_std_model, "ADC12_IN5": adc_model}), + PinResource("PA6", {"PA6": dio_std_model, "ADC12_IN6": adc_model}), + PinResource("PA7", {"PA7": dio_std_model, "ADC12_IN7": adc_model}), + PinResource("PB0", {"PB0": dio_std_model, "ADC12_IN8": adc_model}), + PinResource("PB1", {"PB1": dio_std_model, "ADC12_IN9": adc_model}), + PinResource("PB2", {"PB2": dio_ft_model}), # BOOT1 + PinResource("PB10", {"PB10": dio_ft_model}), + PinResource("PB11", {"PB11": dio_ft_model}), + PinResource("PB12", {"PB12": dio_ft_model}), + PinResource("PB13", {"PB13": dio_ft_model}), + PinResource("PB14", {"PB14": dio_ft_model}), + PinResource("PB15", {"PB15": dio_ft_model}), + PinResource("PA8", {"PA8": dio_ft_model}), + PinResource("PA9", {"PA9": dio_ft_model}), + PinResource("PA10", {"PA10": dio_ft_model}), + PinResource("PA11", {"PA11": dio_ft_model}), + PinResource("PA12", {"PA12": dio_ft_model}), + # PinResource('PA13', {'PA13': dio_ft_model}), # forced SWDIO default is JTMS/SWDIO + # PinResource('PA14', {'PA14': dio_ft_model}), # forced SWCLK, default is JTCK/SWCLK + PinResource("PA15", {"PA15": dio_ft_model}), # default is JTDI + PinResource("PB3", {"PB3": dio_ft_model}), # SWO, default is JTDO + PinResource("PB4", {"PB4": dio_ft_model}), # default is JNTRST + PinResource("PB5", {"PB5": dio_std_model}), + PinResource("PB6", {"PB6": dio_ft_model}), + PinResource("PB7", {"PB7": dio_ft_model}), + PinResource("PB8", {"PB8": dio_ft_model}), + PinResource("PB9", {"PB9": dio_ft_model}), + # PinResource('NRST', {'NRST': dio_std_model}), # non-mappable to IO! + # de-prioritize these for auto-assignment since they're low-current + PinResource("PC13", {"PC13": dio_pc_13_14_15_model}), + PinResource("PC14", {"PC14": dio_pc_13_14_15_model, "OSC32_IN": Passive()}), + PinResource("PC15", {"PC15": dio_pc_13_14_15_model, "OSC32_OUT": Passive()}), + PeripheralFixedResource("USART2", uart_model, {"tx": ["PA2", "PD5"], "rx": ["PA3", "PD6"]}), + PeripheralFixedResource( + "SPI1", spi_model, {"sck": ["PA5", "PB3"], "miso": ["PA6", "PB4"], "mosi": ["PA7", "PB5"]} + ), + PeripheralFixedResource( + "USART3", uart_model, {"tx": ["PB10", "PD8", "PC10"], "rx": ["PB11", "PD9", "PC11"]} + ), + PeripheralFixedResource("I2C2", i2c_model, {"scl": ["PB10"], "sda": ["PB11"]}), + PeripheralFixedResource( + "I2C2_T", + i2c_target_model, + {"scl": ["PB10"], "sda": ["PB11"]}, # TODO shared resource w/ I2C controller + ), + PeripheralFixedResource("SPI2", spi_model, {"sck": ["PB13"], "miso": ["PB14"], "mosi": ["PB15"]}), + PeripheralFixedResource("USART1", uart_model, {"tx": ["PA9", "PB6"], "rx": ["PA10", "PB7"]}), + PeripheralFixedResource( + "CAN", + CanControllerPort(DigitalBidir.empty()), + {"txd": ["PA12", "PD1", "PB9"], "rxd": ["PA11", "PD0", "PB8"]}, + ), + PeripheralFixedResource("USB", UsbDevicePort(DigitalBidir.empty()), {"dm": ["PA11"], "dp": ["PA12"]}), + PeripheralFixedPin( + "SWD", + SwdTargetPort(dio_std_model), + { # TODO most are FT pins + "swdio": "PA13", + "swclk": "PA14", # note: SWO is PB3 + }, + ), + PeripheralFixedResource("I2C1", i2c_model, {"scl": ["PB6", "PB8"], "sda": ["PB7", "PB9"]}), + PeripheralFixedResource( + "I2C1_T", + i2c_target_model, + {"scl": ["PB6", "PB8"], "sda": ["PB7", "PB9"]}, # TODO shared resource w/ I2C controller + ), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + self.PACKAGE, + self._make_pinning(), + mfr="STMicroelectronics", + part=self.PART, + datasheet="https://www.st.com/resource/en/datasheet/stm32f103c8.pdf", + ) + self.assign(self.lcsc_part, self.LCSC_PART) + self.assign(self.actual_basic_part, self.LCSC_BASIC_PART) class Stm32f103_48_Device(Stm32f103Base_Device): - SYSTEM_PIN_REMAP = { - 'Vbat': '1', - 'VddA': '9', - 'VssA': '8', - 'Vss': ['23', '35', '47'], - 'Vdd': ['24', '36', '48'], - 'BOOT0': '44', - 'OSC_IN': '5', - 'OSC_OUT': '6', - 'NRST': '7', - } - RESOURCE_PIN_REMAP = { - 'PC13': '2', - 'PC14': '3', - 'PC15': '4', - - 'PA0': '10', - 'PA1': '11', - 'PA2': '12', - 'PA3': '13', - 'PA4': '14', - 'PA5': '15', - 'PA6': '16', - 'PA7': '17', - 'PB0': '18', - 'PB1': '19', - - 'PB2': '20', - 'PB10': '21', - 'PB11': '22', - 'PB12': '25', - 'PB13': '26', - 'PB14': '27', - 'PB15': '28', - - 'PA8': '29', - 'PA9': '30', - 'PA10': '31', - 'PA11': '32', - 'PA12': '33', - 'PA13': '34', - - 'PA14': '37', - 'PA15': '38', - 'PB3': '39', - 'PB4': '40', - 'PB5': '41', - 'PB6': '42', - 'PB7': '43', - - 'PB8': '45', - 'PB9': '46', - } - PACKAGE = 'Package_QFP:LQFP-48_7x7mm_P0.5mm' - PART = 'STM32F103xxT6' - LCSC_PART = 'C8734' # C8T6 variant - basic part - # C77994 for GD32F103C8T6, probably mostly drop-in compatible, NOT basic part - LCSC_BASIC_PART = True + SYSTEM_PIN_REMAP = { + "Vbat": "1", + "VddA": "9", + "VssA": "8", + "Vss": ["23", "35", "47"], + "Vdd": ["24", "36", "48"], + "BOOT0": "44", + "OSC_IN": "5", + "OSC_OUT": "6", + "NRST": "7", + } + RESOURCE_PIN_REMAP = { + "PC13": "2", + "PC14": "3", + "PC15": "4", + "PA0": "10", + "PA1": "11", + "PA2": "12", + "PA3": "13", + "PA4": "14", + "PA5": "15", + "PA6": "16", + "PA7": "17", + "PB0": "18", + "PB1": "19", + "PB2": "20", + "PB10": "21", + "PB11": "22", + "PB12": "25", + "PB13": "26", + "PB14": "27", + "PB15": "28", + "PA8": "29", + "PA9": "30", + "PA10": "31", + "PA11": "32", + "PA12": "33", + "PA13": "34", + "PA14": "37", + "PA15": "38", + "PB3": "39", + "PB4": "40", + "PB5": "41", + "PB6": "42", + "PB7": "43", + "PB8": "45", + "PB9": "46", + } + PACKAGE = "Package_QFP:LQFP-48_7x7mm_P0.5mm" + PART = "STM32F103xxT6" + LCSC_PART = "C8734" # C8T6 variant - basic part + # C77994 for GD32F103C8T6, probably mostly drop-in compatible, NOT basic part + LCSC_BASIC_PART = True class UsbDpPullUp(InternalSubcircuit, Block): - def __init__(self, resistance: RangeLike): - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.usb = self.Port(UsbPassivePort.empty(), [InOut]) + def __init__(self, resistance: RangeLike): + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.usb = self.Port(UsbPassivePort.empty(), [InOut]) - self.dp = self.Block(Resistor(resistance)) - self.connect(self.dp.a.adapt_to(VoltageSink()), self.pwr) - self.connect(self.usb.dp, self.dp.b.adapt_to(DigitalBidir())) # ideal - self.usb.dm.init_from(DigitalBidir()) # ideal + self.dp = self.Block(Resistor(resistance)) + self.connect(self.dp.a.adapt_to(VoltageSink()), self.pwr) + self.connect(self.usb.dp, self.dp.b.adapt_to(DigitalBidir())) # ideal + self.usb.dm.init_from(DigitalBidir()) # ideal @abstract_block -class Stm32f103Base(Resettable, IoControllerI2cTarget, IoControllerCan, IoControllerUsb, Microcontroller, - IoControllerWithSwdTargetConnector, WithCrystalGenerator, IoControllerPowerRequired, - BaseIoControllerExportable, GeneratorBlock): - DEVICE: Type[Stm32f103Base_Device] = Stm32f103Base_Device - DEFAULT_CRYSTAL_FREQUENCY = 12*MHertz(tol=0.005) - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.ic: Stm32f103Base_Device - self.generator_param(self.reset.is_connected()) - - @override - def contents(self) -> None: - super().contents() - - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.ic = imp.Block(self.DEVICE(pin_assigns=ArrayStringExpr())) - self.connect(self.xtal_node, self.ic.osc) - self.connect(self.swd_node, self.ic.swd) - self.connect(self.reset_node, self.ic.nrst) - - self.pwr_cap = ElementDict[DecouplingCapacitor]() - # one 0.1uF cap each for Vdd1-5 and one bulk 4.7uF cap - self.pwr_cap[0] = imp.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))) - for i in range(1, 4): - self.pwr_cap[i] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - - # one 10nF and 1uF cap for VddA TODO generate the same cap if a different Vref is used - self.vdda_cap_0 = imp.Block(DecouplingCapacitor(10 * nFarad(tol=0.2))) - self.vdda_cap_1 = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))) - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.nrst) - - - ExportType = TypeVar('ExportType', bound=Port) - @override - def _make_export_vector(self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, - assign: Optional[str]) -> Optional[str]: - if isinstance(self_io, UsbDevicePort): # assumed at most one USB port generates - inner_io = inner_vector.request(name) - self.usb_pull = self.Block(UsbDpPullUp(resistance=1.5*kOhm(tol=0.01))) # required by datasheet Table 44 # TODO proper tolerancing? - self.connect(self.usb_pull.pwr, self.pwr) - self.connect(inner_io, self_io, self.usb_pull.usb) - return assign - return super()._make_export_vector(self_io, inner_vector, name, assign) - - @override - def _crystal_required(self) -> bool: # crystal needed for CAN or USB b/c tighter freq tolerance - return len(self.get(self.can.requested())) > 0 or len(self.get(self.usb.requested())) > 0 \ - or super()._crystal_required() +class Stm32f103Base( + Resettable, + IoControllerI2cTarget, + IoControllerCan, + IoControllerUsb, + Microcontroller, + IoControllerWithSwdTargetConnector, + WithCrystalGenerator, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): + DEVICE: Type[Stm32f103Base_Device] = Stm32f103Base_Device + DEFAULT_CRYSTAL_FREQUENCY = 12 * MHertz(tol=0.005) + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.ic: Stm32f103Base_Device + self.generator_param(self.reset.is_connected()) + + @override + def contents(self) -> None: + super().contents() + + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.ic = imp.Block(self.DEVICE(pin_assigns=ArrayStringExpr())) + self.connect(self.xtal_node, self.ic.osc) + self.connect(self.swd_node, self.ic.swd) + self.connect(self.reset_node, self.ic.nrst) + + self.pwr_cap = ElementDict[DecouplingCapacitor]() + # one 0.1uF cap each for Vdd1-5 and one bulk 4.7uF cap + self.pwr_cap[0] = imp.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))) + for i in range(1, 4): + self.pwr_cap[i] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + + # one 10nF and 1uF cap for VddA TODO generate the same cap if a different Vref is used + self.vdda_cap_0 = imp.Block(DecouplingCapacitor(10 * nFarad(tol=0.2))) + self.vdda_cap_1 = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))) + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.nrst) + + ExportType = TypeVar("ExportType", bound=Port) + + @override + def _make_export_vector( + self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, assign: Optional[str] + ) -> Optional[str]: + if isinstance(self_io, UsbDevicePort): # assumed at most one USB port generates + inner_io = inner_vector.request(name) + self.usb_pull = self.Block( + UsbDpPullUp(resistance=1.5 * kOhm(tol=0.01)) + ) # required by datasheet Table 44 # TODO proper tolerancing? + self.connect(self.usb_pull.pwr, self.pwr) + self.connect(inner_io, self_io, self.usb_pull.usb) + return assign + return super()._make_export_vector(self_io, inner_vector, name, assign) + + @override + def _crystal_required(self) -> bool: # crystal needed for CAN or USB b/c tighter freq tolerance + return ( + len(self.get(self.can.requested())) > 0 + or len(self.get(self.usb.requested())) > 0 + or super()._crystal_required() + ) class Stm32f103_48(Stm32f103Base): - DEVICE = Stm32f103_48_Device + DEVICE = Stm32f103_48_Device diff --git a/edg/parts/Microcontroller_Stm32f303.py b/edg/parts/Microcontroller_Stm32f303.py index e4979e6d1..07892ed73 100644 --- a/edg/parts/Microcontroller_Stm32f303.py +++ b/edg/parts/Microcontroller_Stm32f303.py @@ -8,266 +8,290 @@ @non_library class Stm32f303_Ios(IoControllerI2cTarget, IoControllerDac, IoControllerCan, BaseIoControllerPinmapGenerator): - """Base class for STM32F303x6/8 devices (separate from STM32F303xB/C). - Unlike other microcontrollers, this one also supports dev boards (Nucleo-32) which can be - a power source, so there's a bit more complexity here.""" - RESOURCE_PIN_REMAP: Dict[str, str] + """Base class for STM32F303x6/8 devices (separate from STM32F303xB/C). + Unlike other microcontrollers, this one also supports dev boards (Nucleo-32) which can be + a power source, so there's a bit more complexity here.""" - @abstractmethod - def _vddio_vdda(self) -> Tuple[Port[VoltageLink], Port[VoltageLink]]: - """Returns VDDIO, VDDA (either can be VoltageSink or VoltageSource).""" - ... + RESOURCE_PIN_REMAP: Dict[str, str] - def _vdd_model(self) -> VoltageSink: - return VoltageSink( # assumes single-rail module - voltage_limits=(2, 3.6)*Volt, # table 19 - current_draw=(0.00055, 80)*mAmp + self.io_current_draw.upper() # table 25 Idd standby to max - ) + @abstractmethod + def _vddio_vdda(self) -> Tuple[Port[VoltageLink], Port[VoltageLink]]: + """Returns VDDIO, VDDA (either can be VoltageSink or VoltageSource).""" + ... - @override - def _io_pinmap(self) -> PinMapUtil: - """Returns the mappable for a STM32F303 device with the input power and ground references. - This allows a shared definition between discrete chips and microcontroller boards""" - # these are common to all IO blocks - vdd, vdda = self._vddio_vdda() + def _vdd_model(self) -> VoltageSink: + return VoltageSink( # assumes single-rail module + voltage_limits=(2, 3.6) * Volt, # table 19 + current_draw=(0.00055, 80) * mAmp + self.io_current_draw.upper(), # table 25 Idd standby to max + ) - input_threshold_factor = (0.3, 0.7) # TODO relaxed (but more complex) bounds available for different IO blocks - current_limits = (-20, 20)*mAmp # Section 6.3.14, TODO loose with relaxed VOL/VOH - dio_tc_model = DigitalBidir.from_supply( - self.gnd, vdd, - voltage_limit_abs=(-0.3, 0.3) * Volt, # Table 19 - current_limits=current_limits, - input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True - ) - dio_tc_switch_model = DigitalBidir.from_supply( - self.gnd, vdd, - voltage_limit_abs=(-0.3, 0.3) * Volt, # Table 19 - current_limits=(-3, 0), # Table 13, note 1, can sink 3 mA and should not source current - input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True - ) - dio_tt_model = DigitalBidir.from_supply( - self.gnd, vdd, - voltage_limit_abs=(-0.3, 3.6) * Volt, # Table 19 - current_limits=current_limits, - input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True - ) - dio_tta_model = DigitalBidir.from_supply( - self.gnd, vdd, - voltage_limit_abs=(-0.3 * Volt, vdda.link().voltage.lower() + 0.3 * Volt), # Table 19 - current_limits=current_limits, - input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True - ) - dio_ft_model = DigitalBidir.from_supply( - self.gnd, vdd, - voltage_limit_abs=(-0.3, 5.5) * Volt, # Table 19 - current_limits=current_limits, - input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True - ) - dio_ftf_model = dio_ft_model - dio_boot0_model = DigitalBidir.from_supply( - self.gnd, vdd, - voltage_limit_abs=(-0.3, 5.5) * Volt, # Table 19 - current_limits=current_limits, - input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True - ) + @override + def _io_pinmap(self) -> PinMapUtil: + """Returns the mappable for a STM32F303 device with the input power and ground references. + This allows a shared definition between discrete chips and microcontroller boards""" + # these are common to all IO blocks + vdd, vdda = self._vddio_vdda() - adc_model = AnalogSink.from_supply( - self.gnd, vdd, - voltage_limit_tolerance=(-0.3, 0.3) * Volt, - signal_limit_tolerance=(0, 0), # Table 60 conversion voltage range - impedance=100*kOhm(tol=0) # TODO: actually spec'd as maximum external impedance; internal impedance not given - ) - dac_model = AnalogSource.from_supply( - self.gnd, vdd, - signal_out_bound=(0.2*Volt, -0.2*Volt), - impedance=15*kOhm(tol=0) # assumes buffer off - ) + input_threshold_factor = (0.3, 0.7) # TODO relaxed (but more complex) bounds available for different IO blocks + current_limits = (-20, 20) * mAmp # Section 6.3.14, TODO loose with relaxed VOL/VOH + dio_tc_model = DigitalBidir.from_supply( + self.gnd, + vdd, + voltage_limit_abs=(-0.3, 0.3) * Volt, # Table 19 + current_limits=current_limits, + input_threshold_factor=input_threshold_factor, + pullup_capable=True, + pulldown_capable=True, + ) + dio_tc_switch_model = DigitalBidir.from_supply( + self.gnd, + vdd, + voltage_limit_abs=(-0.3, 0.3) * Volt, # Table 19 + current_limits=(-3, 0), # Table 13, note 1, can sink 3 mA and should not source current + input_threshold_factor=input_threshold_factor, + pullup_capable=True, + pulldown_capable=True, + ) + dio_tt_model = DigitalBidir.from_supply( + self.gnd, + vdd, + voltage_limit_abs=(-0.3, 3.6) * Volt, # Table 19 + current_limits=current_limits, + input_threshold_factor=input_threshold_factor, + pullup_capable=True, + pulldown_capable=True, + ) + dio_tta_model = DigitalBidir.from_supply( + self.gnd, + vdd, + voltage_limit_abs=(-0.3 * Volt, vdda.link().voltage.lower() + 0.3 * Volt), # Table 19 + current_limits=current_limits, + input_threshold_factor=input_threshold_factor, + pullup_capable=True, + pulldown_capable=True, + ) + dio_ft_model = DigitalBidir.from_supply( + self.gnd, + vdd, + voltage_limit_abs=(-0.3, 5.5) * Volt, # Table 19 + current_limits=current_limits, + input_threshold_factor=input_threshold_factor, + pullup_capable=True, + pulldown_capable=True, + ) + dio_ftf_model = dio_ft_model + dio_boot0_model = DigitalBidir.from_supply( + self.gnd, + vdd, + voltage_limit_abs=(-0.3, 5.5) * Volt, # Table 19 + current_limits=current_limits, + input_threshold_factor=input_threshold_factor, + pullup_capable=True, + pulldown_capable=True, + ) - uart_model = UartPort(DigitalBidir.empty()) - spi_model = SpiController(DigitalBidir.empty()) - # TODO SPI peripherals, which have fixed-pin CS lines - i2c_model = I2cController(DigitalBidir.empty()) - i2c_target_model = I2cTarget(DigitalBidir.empty()) + adc_model = AnalogSink.from_supply( + self.gnd, + vdd, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + signal_limit_tolerance=(0, 0), # Table 60 conversion voltage range + impedance=100 + * kOhm(tol=0), # TODO: actually spec'd as maximum external impedance; internal impedance not given + ) + dac_model = AnalogSource.from_supply( + self.gnd, vdd, signal_out_bound=(0.2 * Volt, -0.2 * Volt), impedance=15 * kOhm(tol=0) # assumes buffer off + ) - return PinMapUtil([ # Table 13, partial table for 48-pin only - PinResource('PC13', {'PC13': dio_tc_model}), - PinResource('PC14', {'PC14': dio_tc_model}), # TODO remappable to OSC32_IN - PinResource('PC15', {'PC15': dio_tc_model}), # TODO remappable to OSC32_OUT - PinResource('PF0', {'PF0': dio_ft_model}), # TODO remappable to OSC_OUT - PinResource('PF1', {'PF1': dio_ft_model}), # TODO remappable to OSC_OUT + uart_model = UartPort(DigitalBidir.empty()) + spi_model = SpiController(DigitalBidir.empty()) + # TODO SPI peripherals, which have fixed-pin CS lines + i2c_model = I2cController(DigitalBidir.empty()) + i2c_target_model = I2cTarget(DigitalBidir.empty()) - PinResource('PA0', {'PA0': dio_tta_model, 'ADC1_IN1': adc_model}), - PinResource('PA1', {'PA1': dio_tta_model, 'ADC1_IN2': adc_model}), - PinResource('PA2', {'PA2': dio_tta_model, 'ADC1_IN3': adc_model}), - PinResource('PA3', {'PA3': dio_tta_model, 'ADC1_IN4': adc_model}), - PinResource('PA4', {'PA4': dio_tta_model, 'ADC2_IN1': adc_model, 'DAC1_OUT1': dac_model}), - PinResource('PA5', {'PA5': dio_tta_model, 'ADC2_IN2': adc_model, 'DAC1_OUT2': dac_model}), - - PinResource('PA6', {'PA6': dio_tta_model, 'ADC2_IN3': adc_model, 'DAC2_OUT1': dac_model}), - PinResource('PA7', {'PA7': dio_tta_model, 'ADC2_IN4': adc_model}), - PinResource('PB0', {'PB0': dio_tta_model, 'ADC1_IN11': adc_model}), - PinResource('PB1', {'PB1': dio_tta_model, 'ADC1_IN12': adc_model}), - PinResource('PB2', {'PB2': dio_tta_model, 'ADC2_IN12': adc_model}), - PinResource('PB10', {'PB10': dio_tt_model}), - PinResource('PB11', {'PB11': dio_tta_model}), - PinResource('PB12', {'PB12': dio_tta_model, 'ADC2_IN13': adc_model}), - PinResource('PB13', {'PB13': dio_tta_model, 'ADC1_IN13': adc_model}), - PinResource('PB14', {'PB14': dio_tta_model, 'ADC2_IN14': adc_model}), - - PinResource('PB15', {'PB15': dio_tta_model, 'ADC2_IN15': adc_model}), - PinResource('PA8', {'PA8': dio_ft_model}), - PinResource('PA9', {'PA9': dio_ft_model}), - PinResource('PA10', {'PA10': dio_ft_model}), - PinResource('PA11', {'PA11': dio_ft_model}), - PinResource('PA12', {'PA12': dio_ft_model}), - PinResource('PA13', {'PA13': dio_ft_model}), # also JTMS/SWDAT - - PinResource('PA14', {'PA14': dio_ftf_model}), # also JTCK/SWCLK - PinResource('PA15', {'PA15': dio_ftf_model}), # also JTDI - PinResource('PB3', {'PB3': dio_ft_model}), # also JTDO/TRACESWO - PinResource('PB4', {'PB4': dio_ft_model}), - PinResource('PB5', {'PB5': dio_ft_model}), - PinResource('PB6', {'PB6': dio_ftf_model}), - PinResource('PB7', {'PB7': dio_ftf_model}), - - PinResource('PB8', {'PB8': dio_ftf_model}), - PinResource('PB9', {'PB9': dio_ftf_model}), - - PeripheralFixedResource('USART2', uart_model, { - 'tx': ['PA2', 'PA14', 'PB3'], 'rx': ['PA3', 'PA15', 'PB4'] - }), - PeripheralFixedResource('SPI1', spi_model, { - 'sck': ['PA5', 'PB3'], 'miso': ['PA6', 'PB4'], 'mosi': ['PA7', 'PB5'] - }), - PeripheralFixedResource('USART3', uart_model, { # 1/3 check - 'tx': ['PB10', 'PC10', 'PB9'], 'rx': ['PB11', 'PC11', 'PB8'] - }), - PeripheralFixedResource('USART1', uart_model, { - 'tx': ['PA9', 'PB6'], 'rx': ['PA10', 'PB7'] - }), - PeripheralFixedResource('CAN', CanControllerPort(DigitalBidir.empty()), { - 'tx': ['PA12', 'PB9'], 'rx': ['PA11', 'PB8'] - }), - PeripheralFixedResource('I2C1', i2c_model, { - 'scl': ['PA15', 'PB6', 'PB8'], 'sda': ['PA14', 'PB7', 'PB9'] - }), - PeripheralFixedResource('I2C1_T', i2c_target_model, { - 'scl': ['PA15', 'PB6', 'PB8'], 'sda': ['PA14', 'PB7', 'PB9'] - }), - PeripheralFixedPin('SWD', SwdTargetPort(dio_ft_model), { # TODO some are FTf pins - 'swdio': 'PA13', 'swclk': 'PA14', 'reset': 'NRST' # note: SWO is PB3 - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) + return PinMapUtil( + [ # Table 13, partial table for 48-pin only + PinResource("PC13", {"PC13": dio_tc_model}), + PinResource("PC14", {"PC14": dio_tc_model}), # TODO remappable to OSC32_IN + PinResource("PC15", {"PC15": dio_tc_model}), # TODO remappable to OSC32_OUT + PinResource("PF0", {"PF0": dio_ft_model}), # TODO remappable to OSC_OUT + PinResource("PF1", {"PF1": dio_ft_model}), # TODO remappable to OSC_OUT + PinResource("PA0", {"PA0": dio_tta_model, "ADC1_IN1": adc_model}), + PinResource("PA1", {"PA1": dio_tta_model, "ADC1_IN2": adc_model}), + PinResource("PA2", {"PA2": dio_tta_model, "ADC1_IN3": adc_model}), + PinResource("PA3", {"PA3": dio_tta_model, "ADC1_IN4": adc_model}), + PinResource("PA4", {"PA4": dio_tta_model, "ADC2_IN1": adc_model, "DAC1_OUT1": dac_model}), + PinResource("PA5", {"PA5": dio_tta_model, "ADC2_IN2": adc_model, "DAC1_OUT2": dac_model}), + PinResource("PA6", {"PA6": dio_tta_model, "ADC2_IN3": adc_model, "DAC2_OUT1": dac_model}), + PinResource("PA7", {"PA7": dio_tta_model, "ADC2_IN4": adc_model}), + PinResource("PB0", {"PB0": dio_tta_model, "ADC1_IN11": adc_model}), + PinResource("PB1", {"PB1": dio_tta_model, "ADC1_IN12": adc_model}), + PinResource("PB2", {"PB2": dio_tta_model, "ADC2_IN12": adc_model}), + PinResource("PB10", {"PB10": dio_tt_model}), + PinResource("PB11", {"PB11": dio_tta_model}), + PinResource("PB12", {"PB12": dio_tta_model, "ADC2_IN13": adc_model}), + PinResource("PB13", {"PB13": dio_tta_model, "ADC1_IN13": adc_model}), + PinResource("PB14", {"PB14": dio_tta_model, "ADC2_IN14": adc_model}), + PinResource("PB15", {"PB15": dio_tta_model, "ADC2_IN15": adc_model}), + PinResource("PA8", {"PA8": dio_ft_model}), + PinResource("PA9", {"PA9": dio_ft_model}), + PinResource("PA10", {"PA10": dio_ft_model}), + PinResource("PA11", {"PA11": dio_ft_model}), + PinResource("PA12", {"PA12": dio_ft_model}), + PinResource("PA13", {"PA13": dio_ft_model}), # also JTMS/SWDAT + PinResource("PA14", {"PA14": dio_ftf_model}), # also JTCK/SWCLK + PinResource("PA15", {"PA15": dio_ftf_model}), # also JTDI + PinResource("PB3", {"PB3": dio_ft_model}), # also JTDO/TRACESWO + PinResource("PB4", {"PB4": dio_ft_model}), + PinResource("PB5", {"PB5": dio_ft_model}), + PinResource("PB6", {"PB6": dio_ftf_model}), + PinResource("PB7", {"PB7": dio_ftf_model}), + PinResource("PB8", {"PB8": dio_ftf_model}), + PinResource("PB9", {"PB9": dio_ftf_model}), + PeripheralFixedResource( + "USART2", uart_model, {"tx": ["PA2", "PA14", "PB3"], "rx": ["PA3", "PA15", "PB4"]} + ), + PeripheralFixedResource( + "SPI1", spi_model, {"sck": ["PA5", "PB3"], "miso": ["PA6", "PB4"], "mosi": ["PA7", "PB5"]} + ), + PeripheralFixedResource( + "USART3", uart_model, {"tx": ["PB10", "PC10", "PB9"], "rx": ["PB11", "PC11", "PB8"]} # 1/3 check + ), + PeripheralFixedResource("USART1", uart_model, {"tx": ["PA9", "PB6"], "rx": ["PA10", "PB7"]}), + PeripheralFixedResource( + "CAN", CanControllerPort(DigitalBidir.empty()), {"tx": ["PA12", "PB9"], "rx": ["PA11", "PB8"]} + ), + PeripheralFixedResource( + "I2C1", i2c_model, {"scl": ["PA15", "PB6", "PB8"], "sda": ["PA14", "PB7", "PB9"]} + ), + PeripheralFixedResource( + "I2C1_T", i2c_target_model, {"scl": ["PA15", "PB6", "PB8"], "sda": ["PA14", "PB7", "PB9"]} + ), + PeripheralFixedPin( + "SWD", + SwdTargetPort(dio_ft_model), + {"swdio": "PA13", "swclk": "PA14", "reset": "NRST"}, # TODO some are FTf pins # note: SWO is PB3 + ), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) @non_library class Stm32f303_Device(Stm32f303_Ios, IoController, InternalSubcircuit, GeneratorBlock, FootprintBlock): - """STM32F303 chip. - TODO IMPLEMENT ME""" - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] + """STM32F303 chip. + TODO IMPLEMENT ME""" + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ - # 'Vbat': self.vdd, - # 'VddA': self.vdda, - # 'VssA': self.gnd, - 'Vss': self.gnd, - 'Vdd': self.pwr - # 'BOOT0': self.gnd, - }).remap(self.SYSTEM_PIN_REMAP) + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return VariantPinRemapper( + { + # 'Vbat': self.vdd, + # 'VddA': self.vdda, + # 'VssA': self.gnd, + "Vss": self.gnd, + "Vdd": self.pwr, + # 'BOOT0': self.gnd, + } + ).remap(self.SYSTEM_PIN_REMAP) -class Nucleo_F303k8(IoControllerUsbOut, IoControllerPowerOut, IoController, Stm32f303_Ios, GeneratorBlock, - FootprintBlock): - """Nucleo32 F303K8 configured as power source from USB.""" - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'Vss': ['4', '17'], - 'Vdd': '29', - 'Vusb': '19', - 'Vin': '16', - } - RESOURCE_PIN_REMAP = { - 'PA9': '1', # CN3.1, D1 - 'PA10': '2', # CN3.2, D0 - # 'NRST': '3' # CN3.3, RESET - 'PA12': '5', # CN3.5, D2 - 'PB0': '6', # CN3.6, D3 - 'PB7': '7', # CN3.7, D4 - 'PB6': '8', # CN3.8, D5 - 'PB1': '9', # CN3.9, D6 - 'PF0': '10', # CN3.10, D7 - 'PF1': '11', # CN3.11, D8 - 'PA8': '12', # CN3.12, D9 - 'PA11': '13', # CN3.13, D10 - 'PB5': '14', # CN3.14, D11 - 'PB4': '15', # CN3.15, D12 +class Nucleo_F303k8( + IoControllerUsbOut, IoControllerPowerOut, IoController, Stm32f303_Ios, GeneratorBlock, FootprintBlock +): + """Nucleo32 F303K8 configured as power source from USB.""" - # 'NRST': '18' # CN4.3, RESET - 'PA2': '20', # CN4.5, A7 - 'PA7': '21', # CN4.6, A6 - 'PA6': '22', # CN4.7, A5 - 'PA5': '23', # CN4.8, A4 - 'PA4': '24', # CN4.9, A3 - 'PA3': '25', # CN4.10, A2 - 'PA1': '26', # CN4.11, A1 - 'PA0': '27', # CN4.12, A0 - 'PB3': '30', # CN4.15, D13 - } + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "Vss": ["4", "17"], + "Vdd": "29", + "Vusb": "19", + "Vin": "16", + } + RESOURCE_PIN_REMAP = { + "PA9": "1", # CN3.1, D1 + "PA10": "2", # CN3.2, D0 + # 'NRST': '3' # CN3.3, RESET + "PA12": "5", # CN3.5, D2 + "PB0": "6", # CN3.6, D3 + "PB7": "7", # CN3.7, D4 + "PB6": "8", # CN3.8, D5 + "PB1": "9", # CN3.9, D6 + "PF0": "10", # CN3.10, D7 + "PF1": "11", # CN3.11, D8 + "PA8": "12", # CN3.12, D9 + "PA11": "13", # CN3.13, D10 + "PB5": "14", # CN3.14, D11 + "PB4": "15", # CN3.15, D12 + # 'NRST': '18' # CN4.3, RESET + "PA2": "20", # CN4.5, A7 + "PA7": "21", # CN4.6, A6 + "PA6": "22", # CN4.7, A5 + "PA5": "23", # CN4.8, A4 + "PA4": "24", # CN4.9, A3 + "PA3": "25", # CN4.10, A2 + "PA1": "26", # CN4.11, A1 + "PA0": "27", # CN4.12, A0 + "PB3": "30", # CN4.15, D13 + } - @override - def _vddio_vdda(self) -> Tuple[Port[VoltageLink], Port[VoltageLink]]: - if self.get(self.pwr.is_connected()): # board sinks power - return self.pwr, self.pwr - else: - return self.pwr_out, self.pwr_out + @override + def _vddio_vdda(self) -> Tuple[Port[VoltageLink], Port[VoltageLink]]: + if self.get(self.pwr.is_connected()): # board sinks power + return self.pwr, self.pwr + else: + return self.pwr_out, self.pwr_out - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - if self.get(self.pwr.is_connected()): # board sinks power - self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") - self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") - return VariantPinRemapper({ - 'Vdd': self.pwr, - 'Vss': self.gnd, - }).remap(self.SYSTEM_PIN_REMAP) - else: # board sources power (default) - return VariantPinRemapper({ - 'Vdd': self.pwr_out, - 'Vss': self.gnd, - 'Vusb': self.vusb_out, - }).remap(self.SYSTEM_PIN_REMAP) + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + if self.get(self.pwr.is_connected()): # board sinks power + self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") + self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") + return VariantPinRemapper( + { + "Vdd": self.pwr, + "Vss": self.gnd, + } + ).remap(self.SYSTEM_PIN_REMAP) + else: # board sources power (default) + return VariantPinRemapper( + { + "Vdd": self.pwr_out, + "Vss": self.gnd, + "Vusb": self.vusb_out, + } + ).remap(self.SYSTEM_PIN_REMAP) - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.gnd.init_from(Ground()) - self.pwr.init_from(self._vdd_model()) + self.gnd.init_from(Ground()) + self.pwr.init_from(self._vdd_model()) - self.vusb_out.init_from(VoltageSource( - voltage_out=(4.75 - 0.58, 5.1) * Volt, # 4.75V USB - 0.58v BAT60JFILM drop to 5.1 from LD1117S50TR, ignoring ST890CDR - current_limits=(0, 0.5) * Amp # max USB draw # TODO higher from external power - )) - self.pwr_out.init_from(VoltageSource( - voltage_out=3.3 * Volt(tol=0.03), # LD39050PU33R worst-case Vout accuracy - current_limits=(0, 0.5) * Amp # max USB current draw, LDO also guarantees 500mA output current - )) + self.vusb_out.init_from( + VoltageSource( + voltage_out=(4.75 - 0.58, 5.1) + * Volt, # 4.75V USB - 0.58v BAT60JFILM drop to 5.1 from LD1117S50TR, ignoring ST890CDR + current_limits=(0, 0.5) * Amp, # max USB draw # TODO higher from external power + ) + ) + self.pwr_out.init_from( + VoltageSource( + voltage_out=3.3 * Volt(tol=0.03), # LD39050PU33R worst-case Vout accuracy + current_limits=(0, 0.5) * Amp, # max USB current draw, LDO also guarantees 500mA output current + ) + ) - self.generator_param(self.pwr.is_connected()) + self.generator_param(self.pwr.is_connected()) - @override - def generate(self) -> None: - super().generate() + @override + def generate(self) -> None: + super().generate() - self.footprint( - 'U', 'edg:Nucleo32', - self._make_pinning(), - mfr='STMicroelectronics', part='NUCLEO-F303K8', - datasheet='https://www.st.com/resource/en/user_manual/dm00231744.pdf', - ) + self.footprint( + "U", + "edg:Nucleo32", + self._make_pinning(), + mfr="STMicroelectronics", + part="NUCLEO-F303K8", + datasheet="https://www.st.com/resource/en/user_manual/dm00231744.pdf", + ) diff --git a/edg/parts/Microcontroller_Stm32g031.py b/edg/parts/Microcontroller_Stm32g031.py index 731501f36..a74d95550 100644 --- a/edg/parts/Microcontroller_Stm32g031.py +++ b/edg/parts/Microcontroller_Stm32g031.py @@ -7,8 +7,16 @@ @abstract_block -class Stm32g031Base_Device(IoControllerI2cTarget, IoControllerCan, IoControllerUsb, InternalSubcircuit, BaseIoControllerPinmapGenerator, - GeneratorBlock, JlcPart, FootprintBlock): +class Stm32g031Base_Device( + IoControllerI2cTarget, + IoControllerCan, + IoControllerUsb, + InternalSubcircuit, + BaseIoControllerPinmapGenerator, + GeneratorBlock, + JlcPart, + FootprintBlock, +): PACKAGE: str # package name for footprint(...) PART: str # part name for footprint(...) LCSC_PART: str @@ -21,10 +29,13 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) # Additional ports (on top of BaseIoController) - self.pwr = self.Port(VoltageSink( - voltage_limits=(1.7, 3.6)*Volt, # Table 5.3.1 "standard operating voltage", not including Vrefbuf - current_draw=(0.001, 7.6)*mAmp + self.io_current_draw.upper() # Table 25 (run), 30 (standby) - ), [Power]) + self.pwr = self.Port( + VoltageSink( + voltage_limits=(1.7, 3.6) * Volt, # Table 5.3.1 "standard operating voltage", not including Vrefbuf + current_draw=(0.001, 7.6) * mAmp + self.io_current_draw.upper(), # Table 25 (run), 30 (standby) + ), + [Power], + ) self.gnd = self.Port(Ground(), [Common]) self.swd = self.Port(SwdTargetPort.empty()) @@ -34,38 +45,46 @@ def __init__(self, **kwargs: Any) -> None: @override def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ # Pin/peripheral resource definitions (section 4) - 'Vdd': self.pwr, - 'Vss': self.gnd, - 'PF2-NRST': self.nrst, - }).remap(self.SYSTEM_PIN_REMAP) + return VariantPinRemapper( + { # Pin/peripheral resource definitions (section 4) + "Vdd": self.pwr, + "Vss": self.gnd, + "PF2-NRST": self.nrst, + } + ).remap(self.SYSTEM_PIN_REMAP) @override def _io_pinmap(self) -> PinMapUtil: # Port models input_range = self.gnd.link().voltage.hull(self.pwr.link().voltage) - io_voltage_limit = (input_range + (-0.3, 3.6)*Volt).intersect(self.gnd.link().voltage + (-0.3, 5.5)*Volt) + io_voltage_limit = (input_range + (-0.3, 3.6) * Volt).intersect(self.gnd.link().voltage + (-0.3, 5.5) * Volt) dio_ft_model = DigitalBidir.from_supply( - self.gnd, self.pwr, + self.gnd, + self.pwr, voltage_limit_abs=io_voltage_limit, - current_limits=(-15, 15)*mAmp, # Section 5.3.14, relaxed bounds for relaxed Vol/Voh + current_limits=(-15, 15) * mAmp, # Section 5.3.14, relaxed bounds for relaxed Vol/Voh input_threshold_factor=(0.3, 0.7), # Section 5.3.14 - pullup_capable=True, pulldown_capable=True + pullup_capable=True, + pulldown_capable=True, ) dio_fta_model = dio_ftea_model = dio_ftf_model = dio_ftfa_model = dio_ft_model - self.nrst.init_from(DigitalSink.from_supply( # specified differently than other pins - self.gnd, self.pwr, - voltage_limit_abs=io_voltage_limit, # assumed - input_threshold_factor=(0.3, 0.7), - pullup_capable=True # internal pullup - )) + self.nrst.init_from( + DigitalSink.from_supply( # specified differently than other pins + self.gnd, + self.pwr, + voltage_limit_abs=io_voltage_limit, # assumed + input_threshold_factor=(0.3, 0.7), + pullup_capable=True, # internal pullup + ) + ) adc_model = AnalogSink.from_supply( - self.gnd, self.pwr, + self.gnd, + self.pwr, voltage_limit_abs=io_voltage_limit, signal_limit_tolerance=(0, 0), # conversion voltage range, VssA to Vref+ (assumed VddA) - impedance=(50, float('inf'))*kOhm # max external impedance, at lowest listed sampling rate + impedance=(50, float("inf")) * kOhm, # max external impedance, at lowest listed sampling rate ) uart_model = UartPort(DigitalBidir.empty()) @@ -74,136 +93,155 @@ def _io_pinmap(self) -> PinMapUtil: i2c_model = I2cController(DigitalBidir.empty()) i2c_target_model = I2cTarget(DigitalBidir.empty()) - return PinMapUtil([ # Table 12, partial table for up to 32-pin only - PinResource('PC14', {'PC14': dio_ft_model}), # OSC32_IN, OSC_IN (?) - PinResource('PC15', {'PC15': dio_ft_model}), # OSC32_OUT - # PinResource('PF2', {'PF2': dio_ft_model}), # NRST - PinResource('PA0', {'PA0': dio_fta_model, 'ADC_IN0': adc_model}), - PinResource('PA1', {'PA1': dio_ftea_model, 'ADC_IN1': adc_model}), - PinResource('PA2', {'PA2': dio_fta_model, 'ADC_IN2': adc_model}), - PinResource('PA3', {'PA3': dio_ftea_model, 'ADC_IN3': adc_model}), - - PinResource('PA4', {'PA4': dio_fta_model, 'ADC_IN4': adc_model}), - PinResource('PA5', {'PA5': dio_ftea_model, 'ADC_IN5': adc_model}), - PinResource('PA6', {'PA6': dio_ftea_model, 'ADC_IN6': adc_model}), - PinResource('PA7', {'PA7': dio_fta_model, 'ADC_IN7': adc_model}), - PinResource('PB0', {'PB0': dio_ftea_model, 'ADC_IN8': adc_model}), - PinResource('PB1', {'PB1': dio_ftea_model, 'ADC_IN9': adc_model}), - PinResource('PB2', {'PB2': dio_ftea_model, 'ADC_IN10': adc_model}), - PinResource('PA8', {'PA8': dio_ft_model}), - - PinResource('PA9', {'PA9': dio_ftf_model}), - PinResource('PC6', {'PC6': dio_ft_model}), - PinResource('PA10', {'PA10': dio_ftf_model}), - PinResource('PA11', {'PA11': dio_ftfa_model, 'ADC_IN15': adc_model}), - PinResource('PA12', {'PA12': dio_ftfa_model, 'ADC_IN16': adc_model}), - PinResource('PA13', {'PA13': dio_ftea_model, 'ADC_IN17': adc_model}), # SWDIO - # nBOOT_SEL flash bit can be programmed to ignore nBOOT0 bit - PinResource('PA14', {'PA14': dio_fta_model, 'ADC_IN18': adc_model}), # BOOT0, SWCLK, ADC_IN18 - PinResource('PA15', {'PA15': dio_ft_model}), - - PinResource('PB3', {'PB3': dio_ft_model}), - PinResource('PB4', {'PB4': dio_ft_model}), - PinResource('PB5', {'PB5': dio_ft_model}), - PinResource('PB6', {'PB6': dio_ft_model}), - PinResource('PB7', {'PB7': dio_ftea_model, 'ADC_IN11': adc_model}), - PinResource('PB8', {'PB8': dio_ft_model}), - PinResource('PB9', {'PB9': dio_ft_model}), - - PeripheralFixedResource('SPI2', spi_model, { - 'sck': ['PA0', 'PB8'], 'miso': ['PA3', 'PB2', 'PA9', 'PB6'], 'mosi': ['PA4', 'PA10', 'PB7'] - }), - PeripheralFixedResource('SPI1', spi_model, { - 'sck': ['PA1', 'PA5', 'PB3'], 'miso': ['PA6', 'PA11', 'PB4'], 'mosi': ['PA2', 'PA7', 'PA12', 'PB5'] - }), - PeripheralFixedResource('I2S1', I2sController(DigitalBidir.empty()), { - 'sck': ['PA1', 'PA5', 'PB3'], 'ws': ['PA4', 'PB0', 'PA15'], 'sd': ['PA2', 'PA7', 'PA12', 'PB5'] - }), - PeripheralFixedResource('USART2', uart_model, { - 'tx': ['PA2', 'PA14'], 'rx': ['PA3', 'PA15'] - }), - PeripheralFixedResource('LPUART1', uart_model, { - 'tx': ['PA2'], 'rx': ['PA3'] - }), - PeripheralFixedResource('USART1', uart_model, { - 'tx': ['PA9', 'PB6'], 'rx': ['PA10', 'PB7'] - }), - PeripheralFixedResource('I2C1', i2c_model, { - 'scl': ['PA9', 'PB6', 'PB8'], 'sda': ['PA10', 'PB7', 'PB9'] - }), - PeripheralFixedResource('I2C1_T', i2c_target_model, { # TODO shared resource w/ I2C controller - 'scl': ['PA9', 'PB6', 'PB8'], 'sda': ['PA10', 'PB7', 'PB9'] - }), - PeripheralFixedResource('I2C2', i2c_model, { - 'scl': ['PA11'], 'sda': ['PA12'] - }), - PeripheralFixedResource('I2C2_T', i2c_target_model, { # TODO shared resource w/ I2C controller - 'scl': ['PA11'], 'sda': ['PA12'] - }), - PeripheralFixedResource('SWD', SwdTargetPort(DigitalBidir.empty()), { - 'swdio': ['PA13'], 'swclk': ['PA14'], - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) + return PinMapUtil( + [ # Table 12, partial table for up to 32-pin only + PinResource("PC14", {"PC14": dio_ft_model}), # OSC32_IN, OSC_IN (?) + PinResource("PC15", {"PC15": dio_ft_model}), # OSC32_OUT + # PinResource('PF2', {'PF2': dio_ft_model}), # NRST + PinResource("PA0", {"PA0": dio_fta_model, "ADC_IN0": adc_model}), + PinResource("PA1", {"PA1": dio_ftea_model, "ADC_IN1": adc_model}), + PinResource("PA2", {"PA2": dio_fta_model, "ADC_IN2": adc_model}), + PinResource("PA3", {"PA3": dio_ftea_model, "ADC_IN3": adc_model}), + PinResource("PA4", {"PA4": dio_fta_model, "ADC_IN4": adc_model}), + PinResource("PA5", {"PA5": dio_ftea_model, "ADC_IN5": adc_model}), + PinResource("PA6", {"PA6": dio_ftea_model, "ADC_IN6": adc_model}), + PinResource("PA7", {"PA7": dio_fta_model, "ADC_IN7": adc_model}), + PinResource("PB0", {"PB0": dio_ftea_model, "ADC_IN8": adc_model}), + PinResource("PB1", {"PB1": dio_ftea_model, "ADC_IN9": adc_model}), + PinResource("PB2", {"PB2": dio_ftea_model, "ADC_IN10": adc_model}), + PinResource("PA8", {"PA8": dio_ft_model}), + PinResource("PA9", {"PA9": dio_ftf_model}), + PinResource("PC6", {"PC6": dio_ft_model}), + PinResource("PA10", {"PA10": dio_ftf_model}), + PinResource("PA11", {"PA11": dio_ftfa_model, "ADC_IN15": adc_model}), + PinResource("PA12", {"PA12": dio_ftfa_model, "ADC_IN16": adc_model}), + PinResource("PA13", {"PA13": dio_ftea_model, "ADC_IN17": adc_model}), # SWDIO + # nBOOT_SEL flash bit can be programmed to ignore nBOOT0 bit + PinResource("PA14", {"PA14": dio_fta_model, "ADC_IN18": adc_model}), # BOOT0, SWCLK, ADC_IN18 + PinResource("PA15", {"PA15": dio_ft_model}), + PinResource("PB3", {"PB3": dio_ft_model}), + PinResource("PB4", {"PB4": dio_ft_model}), + PinResource("PB5", {"PB5": dio_ft_model}), + PinResource("PB6", {"PB6": dio_ft_model}), + PinResource("PB7", {"PB7": dio_ftea_model, "ADC_IN11": adc_model}), + PinResource("PB8", {"PB8": dio_ft_model}), + PinResource("PB9", {"PB9": dio_ft_model}), + PeripheralFixedResource( + "SPI2", + spi_model, + {"sck": ["PA0", "PB8"], "miso": ["PA3", "PB2", "PA9", "PB6"], "mosi": ["PA4", "PA10", "PB7"]}, + ), + PeripheralFixedResource( + "SPI1", + spi_model, + { + "sck": ["PA1", "PA5", "PB3"], + "miso": ["PA6", "PA11", "PB4"], + "mosi": ["PA2", "PA7", "PA12", "PB5"], + }, + ), + PeripheralFixedResource( + "I2S1", + I2sController(DigitalBidir.empty()), + {"sck": ["PA1", "PA5", "PB3"], "ws": ["PA4", "PB0", "PA15"], "sd": ["PA2", "PA7", "PA12", "PB5"]}, + ), + PeripheralFixedResource("USART2", uart_model, {"tx": ["PA2", "PA14"], "rx": ["PA3", "PA15"]}), + PeripheralFixedResource("LPUART1", uart_model, {"tx": ["PA2"], "rx": ["PA3"]}), + PeripheralFixedResource("USART1", uart_model, {"tx": ["PA9", "PB6"], "rx": ["PA10", "PB7"]}), + PeripheralFixedResource( + "I2C1", i2c_model, {"scl": ["PA9", "PB6", "PB8"], "sda": ["PA10", "PB7", "PB9"]} + ), + PeripheralFixedResource( + "I2C1_T", + i2c_target_model, + { # TODO shared resource w/ I2C controller + "scl": ["PA9", "PB6", "PB8"], + "sda": ["PA10", "PB7", "PB9"], + }, + ), + PeripheralFixedResource("I2C2", i2c_model, {"scl": ["PA11"], "sda": ["PA12"]}), + PeripheralFixedResource( + "I2C2_T", + i2c_target_model, + {"scl": ["PA11"], "sda": ["PA12"]}, # TODO shared resource w/ I2C controller + ), + PeripheralFixedResource( + "SWD", + SwdTargetPort(DigitalBidir.empty()), + { + "swdio": ["PA13"], + "swclk": ["PA14"], + }, + ), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) @override def generate(self) -> None: super().generate() self.footprint( - 'U', self.PACKAGE, + "U", + self.PACKAGE, self._make_pinning(), - mfr='STMicroelectronics', part=self.PART, - datasheet='https://www.st.com/resource/en/datasheet/stm32g031c6.pdf' + mfr="STMicroelectronics", + part=self.PART, + datasheet="https://www.st.com/resource/en/datasheet/stm32g031c6.pdf", ) self.assign(self.lcsc_part, self.LCSC_PART) self.assign(self.actual_basic_part, False) class Stm32g031_G_Device(Stm32g031Base_Device): - """"STM32G031 GxU in UFQFPN28 package.""" + """ "STM32G031 GxU in UFQFPN28 package.""" + SYSTEM_PIN_REMAP = { - 'Vdd': '3', - 'Vss': '4', - 'PF2-NRST': '5', + "Vdd": "3", + "Vss": "4", + "PF2-NRST": "5", } RESOURCE_PIN_REMAP = { - 'PC14': '1', - 'PC15': '2', - 'PA0': '6', - 'PA1': '7', - - 'PA2': '8', - 'PA3': '9', - 'PA4': '10', - 'PA5': '11', - 'PA6': '12', - 'PA7': '13', - 'PB0': '14', - - 'PB1': '15', - 'PA8': '16', - 'PC6': '17', - 'PA11': '18', - 'PA12': '19', - 'PA13': '20', - 'PA14': '21', - - 'PA15': '22', - 'PB3': '23', - 'PB4': '24', - 'PB5': '25', - 'PB6': '26', - 'PB7': '27', - 'PB8': '28', + "PC14": "1", + "PC15": "2", + "PA0": "6", + "PA1": "7", + "PA2": "8", + "PA3": "9", + "PA4": "10", + "PA5": "11", + "PA6": "12", + "PA7": "13", + "PB0": "14", + "PB1": "15", + "PA8": "16", + "PC6": "17", + "PA11": "18", + "PA12": "19", + "PA13": "20", + "PA14": "21", + "PA15": "22", + "PB3": "23", + "PB4": "24", + "PB5": "25", + "PB6": "26", + "PB7": "27", + "PB8": "28", } - PACKAGE = 'Package_DFN_QFN:QFN-28_4x4mm_P0.5mm' - PART = 'STM32G031Gxxx' - LCSC_PART = 'C432211' # G8U6 variant + PACKAGE = "Package_DFN_QFN:QFN-28_4x4mm_P0.5mm" + PART = "STM32G031Gxxx" + LCSC_PART = "C432211" # G8U6 variant @abstract_block -class Stm32g031Base(Resettable, IoControllerI2cTarget, Microcontroller, IoControllerWithSwdTargetConnector, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): +class Stm32g031Base( + Resettable, + IoControllerI2cTarget, + Microcontroller, + IoControllerWithSwdTargetConnector, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): DEVICE: Type[Stm32g031Base_Device] = Stm32g031Base_Device def __init__(self, **kwargs: Any) -> None: @@ -215,10 +253,7 @@ def __init__(self, **kwargs: Any) -> None: def contents(self) -> None: super().contents() - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: self.ic = imp.Block(self.DEVICE(pin_assigns=ArrayStringExpr())) self.connect(self.swd_node, self.ic.swd) self.connect(self.reset_node, self.ic.nrst) diff --git a/edg/parts/Microcontroller_Stm32g431.py b/edg/parts/Microcontroller_Stm32g431.py index cfcaf8438..ea675dee2 100644 --- a/edg/parts/Microcontroller_Stm32g431.py +++ b/edg/parts/Microcontroller_Stm32g431.py @@ -7,9 +7,17 @@ @abstract_block -class Stm32g431Base_Device(IoControllerI2cTarget, IoControllerCan, IoControllerUsb, InternalSubcircuit, IoControllerUsbCc, - BaseIoControllerPinmapGenerator, - GeneratorBlock, JlcPart, FootprintBlock): +class Stm32g431Base_Device( + IoControllerI2cTarget, + IoControllerCan, + IoControllerUsb, + InternalSubcircuit, + IoControllerUsbCc, + BaseIoControllerPinmapGenerator, + GeneratorBlock, + JlcPart, + FootprintBlock, +): PACKAGE: str # package name for footprint(...) PART: str # part name for footprint(...) LCSC_PART: str @@ -22,10 +30,9 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) # Power and ground - self.pwr = self.Port(VoltageSink( - voltage_limits=(1.71, 3.6) * Volt, - current_draw=(14 * nAmp, 44.0 * mAmp) # Table 32 - ), [Power]) + self.pwr = self.Port( + VoltageSink(voltage_limits=(1.71, 3.6) * Volt, current_draw=(14 * nAmp, 44.0 * mAmp)), [Power] # Table 32 + ) self.gnd = self.Port(Ground(), [Common]) @@ -35,168 +42,181 @@ def __init__(self, **kwargs: Any) -> None: @override def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ - 'Vdd': self.pwr, - 'Vss': self.gnd, - # 'VddA': self.pwr, - # 'VssA': self.gnd, - 'BOOT0': self.gnd, - 'PG10-NRST': self.nrst, - }).remap(self.SYSTEM_PIN_REMAP) + return VariantPinRemapper( + { + "Vdd": self.pwr, + "Vss": self.gnd, + # 'VddA': self.pwr, + # 'VssA': self.gnd, + "BOOT0": self.gnd, + "PG10-NRST": self.nrst, + } + ).remap(self.SYSTEM_PIN_REMAP) @override def _io_pinmap(self) -> PinMapUtil: input_range = self.gnd.link().voltage.hull(self.pwr.link().voltage) io_voltage_limit = (input_range + (-0.3, 3.6) * Volt).intersect( - self.gnd.link().voltage + (-0.3, 5.5) * Volt) # Section 5.3.1 + self.gnd.link().voltage + (-0.3, 5.5) * Volt + ) # Section 5.3.1 input_threshold_factor = (0.3, 0.7) # Section 5.3.14 current_limits = (-20, 20) * mAmp # Table 15 dio_ft_model = DigitalBidir.from_supply( - self.gnd, self.pwr, + self.gnd, + self.pwr, voltage_limit_abs=io_voltage_limit, current_limits=current_limits, input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True + pullup_capable=True, + pulldown_capable=True, ) dio_tt_model = DigitalBidir.from_supply( - self.gnd, self.pwr, + self.gnd, + self.pwr, voltage_limit_tolerance=(-0.3, 0.3) * Volt, # Table 19 current_limits=current_limits, input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True + pullup_capable=True, + pulldown_capable=True, ) # Pin definition, 4.10 - dio_fta_model = dio_ftca_model = dio_ftf_model = dio_ftfa_model = \ - dio_ftfu_model = dio_ftfd_model = dio_ftda_model = dio_ftu_model = dio_ft_model + dio_fta_model = dio_ftca_model = dio_ftf_model = dio_ftfa_model = dio_ftfu_model = dio_ftfd_model = ( + dio_ftda_model + ) = dio_ftu_model = dio_ft_model dio_tta_model = dio_tt_model dio_ftc_model = DigitalBidir.from_supply( - self.gnd, self.pwr, + self.gnd, + self.pwr, voltage_limit_abs=(-0.3, 5.0) * Volt, current_limits=current_limits, input_threshold_factor=input_threshold_factor, - pullup_capable=True, pulldown_capable=True + pullup_capable=True, + pulldown_capable=True, ) adc_model = AnalogSink.from_supply( - self.gnd, self.pwr, + self.gnd, + self.pwr, voltage_limit_tolerance=(-0.3, 0.3) * Volt, signal_limit_tolerance=(0, 0), - impedance=(50, float('inf')) * kOhm # TODO: this is maximum external input impedance, maybe restrictive + impedance=(50, float("inf")) * kOhm, # TODO: this is maximum external input impedance, maybe restrictive ) dac_model = AnalogSource.from_supply( - self.gnd, self.pwr, + self.gnd, + self.pwr, signal_out_bound=(0.2 * Volt, -0.2 * Volt), # signal_out_bound only applies when output buffer on - impedance=(9.6, 13.8) * kOhm # assumes buffer off + impedance=(9.6, 13.8) * kOhm, # assumes buffer off + ) + self.nrst.init_from( + DigitalSink.from_supply( # specified differently than other pins + self.gnd, + self.pwr, + voltage_limit_abs=io_voltage_limit, # assumed + input_threshold_factor=(0.3, 0.7), + pullup_capable=True, # internal pullup + ) ) - self.nrst.init_from(DigitalSink.from_supply( # specified differently than other pins - self.gnd, self.pwr, - voltage_limit_abs=io_voltage_limit, # assumed - input_threshold_factor=(0.3, 0.7), - pullup_capable=True # internal pullup - )) uart_model = UartPort(DigitalBidir.empty()) spi_model = SpiController(DigitalBidir.empty()) i2c_model = I2cController(DigitalBidir.empty()) i2c_target_model = I2cTarget(DigitalBidir.empty()) - return PinMapUtil([ # for 32 pins only for now - PinResource('PF0', {'PF0': dio_ftfa_model, 'ADC1_IN10': adc_model}), # TODO remappable to OSC_IN - PinResource('PF1', {'PF1': dio_fta_model, 'ADC2_IN10': adc_model}), # TODO remappable to OSC_OUT - - PinResource('PA0', {'PA0': dio_tta_model, 'ADC12_IN1': adc_model}), - PinResource('PA1', {'PA1': dio_tta_model, 'ADC12_IN2': adc_model}), - PinResource('PA2', {'PA2': dio_tta_model, 'ADC1_IN3': adc_model}), - PinResource('PA3', {'PA3': dio_tta_model, 'ADC1_IN4': adc_model}), - PinResource('PA4', {'PA4': dio_tta_model, 'ADC2_IN17': adc_model, 'DAC1_OUT1': dac_model}), - PinResource('PA5', {'PA5': dio_tta_model, 'ADC2_IN13': adc_model, 'DAC1_OUT2': dac_model}), - PinResource('PA6', {'PA6': dio_tta_model, 'ADC2_IN3': adc_model}), - PinResource('PA7', {'PA7': dio_tta_model, 'ADC2_IN4': adc_model}), - PinResource('PA8', {'PA8': dio_ftf_model}), - PinResource('PA9', {'PA9': dio_ftfd_model}), - PinResource('PA10', {'PA10': dio_ftda_model}), - PinResource('PA11', {'PA11': dio_ftu_model}), # USB_DM - PinResource('PA12', {'PA12': dio_ftu_model}), # USB_DP - PinResource('PA13', {'PA13': dio_ftf_model}), - PinResource('PA14', {'PA14': dio_ftf_model}), - PinResource('PA15', {'PA15': dio_ftf_model}), - - PinResource('PB0', {'PB0': dio_tta_model, 'ADC1_IN15': adc_model}), - PinResource('PB3', {'PB3': dio_ft_model}), - PinResource('PB4', {'PB4': dio_ftc_model}), - PinResource('PB5', {'PB5': dio_ftf_model}), - PinResource('PB6', {'PB6': dio_ftc_model}), - PinResource('PB7', {'PB7': dio_ftf_model}), - PinResource('PB8', {'PB8': dio_ftf_model}), - - # From table 13 - PeripheralFixedResource('SPI1', spi_model, { - 'sck': ['PA5', 'PB3'], 'miso': ['PA6', 'PB4'], 'mosi': ['PA7', 'PB5'] - }), - PeripheralFixedResource('SPI2', spi_model, { - 'sck': ['PF1'], 'miso': ['PA10'], 'mosi': ['PA11'] - }), - PeripheralFixedResource('SPI3', spi_model, { - 'sck': ['PB3'], 'miso': ['PB4'], 'mosi': ['PB5'] - }), - PeripheralFixedResource('I2S2', I2sController(DigitalBidir.empty()), { - 'sck': ['PB13', 'PF1'], 'ws': ['PB12', 'PF0'], 'sd': ['PA11', 'PB15'] - }), - PeripheralFixedResource('I2S3', I2sController(DigitalBidir.empty()), { - 'sck': ['PB3'], 'ws': ['PA4', 'PA15'], 'sd': ['PB5'] - }), - PeripheralFixedResource('USART1', uart_model, { - 'tx': ['PA9', 'PB6'], 'rx': ['PA10', 'PB7'] - }), - PeripheralFixedResource('USART2', uart_model, { - 'tx': ['PA2', 'PA14', 'PB3'], 'rx': ['PA3', 'PA15', 'PB4'] - }), - PeripheralFixedResource('USART3', uart_model, { - 'tx': ['PB9', 'PB10'], 'rx': ['PB8', 'PB11', ] - }), - PeripheralFixedResource('LPUART1', uart_model, { - 'tx': ['PA2', 'PB11'], 'rx': ['PA3', 'PB10'] - }), - PeripheralFixedResource('I2C1', i2c_model, { - 'scl': ['PA13', 'PA15', 'PB8'], 'sda': ['PA14', 'PB7', 'PB9'] - }), - PeripheralFixedResource('I2C1_T', i2c_target_model, { - 'scl': ['PA13', 'PA15', 'PB8'], 'sda': ['PA14', 'PB7', 'PB9'] - }), - PeripheralFixedResource('I2C2', i2c_model, { - 'scl': ['PA9'], 'sda': ['PA8', 'PF0'] - }), - PeripheralFixedResource('I2C2_T', i2c_target_model, { - 'scl': ['PA9'], 'sda': ['PA8', 'PF0'] - }), - PeripheralFixedResource('I2C3', i2c_model, { - 'scl': ['PA8'], 'sda': ['PB5'] - }), - PeripheralFixedResource('FDCAN', CanControllerPort(DigitalBidir.empty()), { - 'tx': ['PA12', 'PB9'], 'rx': ['PA11', 'PB8'] - }), - PeripheralFixedResource('SWD', SwdTargetPort(DigitalBidir.empty()), { - 'swdio': ['PA13'], 'swclk': ['PA14'], - }), - PeripheralFixedResource('USB', UsbDevicePort(DigitalBidir.empty()), { - 'dm': ['PA11'], 'dp': ['PA12'] - }), - PeripheralFixedResource('USBCC', UsbCcPort(pullup_capable=True), { - 'cc1': ['PB6'], 'cc2': ['PB4'] - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) + return PinMapUtil( + [ # for 32 pins only for now + PinResource("PF0", {"PF0": dio_ftfa_model, "ADC1_IN10": adc_model}), # TODO remappable to OSC_IN + PinResource("PF1", {"PF1": dio_fta_model, "ADC2_IN10": adc_model}), # TODO remappable to OSC_OUT + PinResource("PA0", {"PA0": dio_tta_model, "ADC12_IN1": adc_model}), + PinResource("PA1", {"PA1": dio_tta_model, "ADC12_IN2": adc_model}), + PinResource("PA2", {"PA2": dio_tta_model, "ADC1_IN3": adc_model}), + PinResource("PA3", {"PA3": dio_tta_model, "ADC1_IN4": adc_model}), + PinResource("PA4", {"PA4": dio_tta_model, "ADC2_IN17": adc_model, "DAC1_OUT1": dac_model}), + PinResource("PA5", {"PA5": dio_tta_model, "ADC2_IN13": adc_model, "DAC1_OUT2": dac_model}), + PinResource("PA6", {"PA6": dio_tta_model, "ADC2_IN3": adc_model}), + PinResource("PA7", {"PA7": dio_tta_model, "ADC2_IN4": adc_model}), + PinResource("PA8", {"PA8": dio_ftf_model}), + PinResource("PA9", {"PA9": dio_ftfd_model}), + PinResource("PA10", {"PA10": dio_ftda_model}), + PinResource("PA11", {"PA11": dio_ftu_model}), # USB_DM + PinResource("PA12", {"PA12": dio_ftu_model}), # USB_DP + PinResource("PA13", {"PA13": dio_ftf_model}), + PinResource("PA14", {"PA14": dio_ftf_model}), + PinResource("PA15", {"PA15": dio_ftf_model}), + PinResource("PB0", {"PB0": dio_tta_model, "ADC1_IN15": adc_model}), + PinResource("PB3", {"PB3": dio_ft_model}), + PinResource("PB4", {"PB4": dio_ftc_model}), + PinResource("PB5", {"PB5": dio_ftf_model}), + PinResource("PB6", {"PB6": dio_ftc_model}), + PinResource("PB7", {"PB7": dio_ftf_model}), + PinResource("PB8", {"PB8": dio_ftf_model}), + # From table 13 + PeripheralFixedResource( + "SPI1", spi_model, {"sck": ["PA5", "PB3"], "miso": ["PA6", "PB4"], "mosi": ["PA7", "PB5"]} + ), + PeripheralFixedResource("SPI2", spi_model, {"sck": ["PF1"], "miso": ["PA10"], "mosi": ["PA11"]}), + PeripheralFixedResource("SPI3", spi_model, {"sck": ["PB3"], "miso": ["PB4"], "mosi": ["PB5"]}), + PeripheralFixedResource( + "I2S2", + I2sController(DigitalBidir.empty()), + {"sck": ["PB13", "PF1"], "ws": ["PB12", "PF0"], "sd": ["PA11", "PB15"]}, + ), + PeripheralFixedResource( + "I2S3", I2sController(DigitalBidir.empty()), {"sck": ["PB3"], "ws": ["PA4", "PA15"], "sd": ["PB5"]} + ), + PeripheralFixedResource("USART1", uart_model, {"tx": ["PA9", "PB6"], "rx": ["PA10", "PB7"]}), + PeripheralFixedResource( + "USART2", uart_model, {"tx": ["PA2", "PA14", "PB3"], "rx": ["PA3", "PA15", "PB4"]} + ), + PeripheralFixedResource( + "USART3", + uart_model, + { + "tx": ["PB9", "PB10"], + "rx": [ + "PB8", + "PB11", + ], + }, + ), + PeripheralFixedResource("LPUART1", uart_model, {"tx": ["PA2", "PB11"], "rx": ["PA3", "PB10"]}), + PeripheralFixedResource( + "I2C1", i2c_model, {"scl": ["PA13", "PA15", "PB8"], "sda": ["PA14", "PB7", "PB9"]} + ), + PeripheralFixedResource( + "I2C1_T", i2c_target_model, {"scl": ["PA13", "PA15", "PB8"], "sda": ["PA14", "PB7", "PB9"]} + ), + PeripheralFixedResource("I2C2", i2c_model, {"scl": ["PA9"], "sda": ["PA8", "PF0"]}), + PeripheralFixedResource("I2C2_T", i2c_target_model, {"scl": ["PA9"], "sda": ["PA8", "PF0"]}), + PeripheralFixedResource("I2C3", i2c_model, {"scl": ["PA8"], "sda": ["PB5"]}), + PeripheralFixedResource( + "FDCAN", CanControllerPort(DigitalBidir.empty()), {"tx": ["PA12", "PB9"], "rx": ["PA11", "PB8"]} + ), + PeripheralFixedResource( + "SWD", + SwdTargetPort(DigitalBidir.empty()), + { + "swdio": ["PA13"], + "swclk": ["PA14"], + }, + ), + PeripheralFixedResource("USB", UsbDevicePort(DigitalBidir.empty()), {"dm": ["PA11"], "dp": ["PA12"]}), + PeripheralFixedResource("USBCC", UsbCcPort(pullup_capable=True), {"cc1": ["PB6"], "cc2": ["PB4"]}), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) @override def generate(self) -> None: super().generate() self.footprint( - 'U', self.PACKAGE, + "U", + self.PACKAGE, self._make_pinning(), - mfr='STMicroelectronics', part=self.PART, - datasheet='https://www.st.com/resource/en/datasheet/stm32g431kb.pdf' + mfr="STMicroelectronics", + part=self.PART, + datasheet="https://www.st.com/resource/en/datasheet/stm32g431kb.pdf", ) self.assign(self.lcsc_part, self.LCSC_PART) self.assign(self.actual_basic_part, False) @@ -204,45 +224,52 @@ def generate(self) -> None: class Stm32g431_G_Device(Stm32g431Base_Device): SYSTEM_PIN_REMAP = { - 'Vdd': ['1', '15', '17'], # 15 VDDA - 'Vss': ['14', '16', '32'], # 14 VSSA - 'BOOT0': '31', - 'PG10-NRST': '4', + "Vdd": ["1", "15", "17"], # 15 VDDA + "Vss": ["14", "16", "32"], # 14 VSSA + "BOOT0": "31", + "PG10-NRST": "4", } RESOURCE_PIN_REMAP = { - 'PF0': '2', - 'PF1': '3', - 'PA0': '5', - 'PA1': '6', - 'PA2': '7', - 'PA3': '8', - 'PA4': '9', - 'PA5': '10', - 'PA6': '11', - 'PA7': '12', - 'PA8': '18', - 'PA9': '19', - 'PA10': '20', - 'PA11': '21', - 'PA12': '22', - 'PA13': '23', - 'PA14': '24', - 'PA15': '25', - 'PB0': '13', - 'PB3': '26', - 'PB4': '27', - 'PB5': '28', - 'PB6': '29', - 'PB7': '30', + "PF0": "2", + "PF1": "3", + "PA0": "5", + "PA1": "6", + "PA2": "7", + "PA3": "8", + "PA4": "9", + "PA5": "10", + "PA6": "11", + "PA7": "12", + "PA8": "18", + "PA9": "19", + "PA10": "20", + "PA11": "21", + "PA12": "22", + "PA13": "23", + "PA14": "24", + "PA15": "25", + "PB0": "13", + "PB3": "26", + "PB4": "27", + "PB5": "28", + "PB6": "29", + "PB7": "30", } - PACKAGE = 'Package_DFN_QFN:UFQFPN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm' - PART = 'STM32G431KB' - LCSC_PART = 'C1341901' # STM32G431KBU3 + PACKAGE = "Package_DFN_QFN:UFQFPN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm" + PART = "STM32G431KB" + LCSC_PART = "C1341901" # STM32G431KBU3 @abstract_block -class Stm32g431Base(Resettable, IoControllerI2cTarget, Microcontroller, IoControllerWithSwdTargetConnector, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): +class Stm32g431Base( + Resettable, + IoControllerI2cTarget, + Microcontroller, + IoControllerWithSwdTargetConnector, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): DEVICE: Type[Stm32g431Base_Device] = Stm32g431Base_Device def __init__(self, **kwargs: Any) -> None: @@ -253,10 +280,7 @@ def __init__(self, **kwargs: Any) -> None: @override def contents(self) -> None: super().contents() - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: self.ic = imp.Block(self.DEVICE(pin_assigns=ArrayStringExpr())) self.connect(self.swd_node, self.ic.swd) self.connect(self.reset_node, self.ic.nrst) diff --git a/edg/parts/Microcontroller_Stm32l432.py b/edg/parts/Microcontroller_Stm32l432.py index 164e8b7e2..e02e52620 100644 --- a/edg/parts/Microcontroller_Stm32l432.py +++ b/edg/parts/Microcontroller_Stm32l432.py @@ -7,8 +7,17 @@ @abstract_block -class Stm32l432Base_Device(IoControllerI2cTarget, IoControllerDac, IoControllerCan, IoControllerUsb, InternalSubcircuit, - BaseIoControllerPinmapGenerator, GeneratorBlock, JlcPart, FootprintBlock): +class Stm32l432Base_Device( + IoControllerI2cTarget, + IoControllerDac, + IoControllerCan, + IoControllerUsb, + InternalSubcircuit, + BaseIoControllerPinmapGenerator, + GeneratorBlock, + JlcPart, + FootprintBlock, +): PACKAGE: str # package name for footprint(...) PART: str # part name for footprint(...) LCSC_PART: str @@ -22,12 +31,15 @@ def __init__(self, **kwargs: Any) -> None: # Additional ports (on top of BaseIoController) self.gnd = self.Port(Ground(), [Common]) - self.pwr = self.Port(VoltageSink( - voltage_limits=(self.usb.length() > 0).then_else((3.0, 3.6)*Volt, - (self.dac.length() > 0).then_else((1.8, 3.6)*Volt, - (1.71, 3.6)*Volt)), - current_draw=(0.00000782, 10.3)*mAmp + self.io_current_draw.upper() # Table 25 (run), 37 (shutdown) - ), [Power]) + self.pwr = self.Port( + VoltageSink( + voltage_limits=(self.usb.length() > 0).then_else( + (3.0, 3.6) * Volt, (self.dac.length() > 0).then_else((1.8, 3.6) * Volt, (1.71, 3.6) * Volt) + ), + current_draw=(0.00000782, 10.3) * mAmp + self.io_current_draw.upper(), # Table 25 (run), 37 (shutdown) + ), + [Power], + ) self.swd = self.Port(SwdTargetPort.empty()) self._io_ports.insert(0, self.swd) @@ -35,52 +47,61 @@ def __init__(self, **kwargs: Any) -> None: @override def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ # Pin/peripheral resource definitions (section 4) - 'Vdd': self.pwr, - 'VddA': self.pwr, - 'Vss': self.gnd, - 'NRST': self.nrst, - }).remap(self.SYSTEM_PIN_REMAP) + return VariantPinRemapper( + { # Pin/peripheral resource definitions (section 4) + "Vdd": self.pwr, + "VddA": self.pwr, + "Vss": self.gnd, + "NRST": self.nrst, + } + ).remap(self.SYSTEM_PIN_REMAP) @override def _io_pinmap(self) -> PinMapUtil: # Port models input_range = self.gnd.link().voltage.hull(self.pwr.link().voltage) # tt is 3.6v tolerant IO, ft (and all except tt_xx) is 5v tolerant IO - tt_voltage_limit = input_range + (-0.3, 0.3)*Volt - io_voltage_limit = (input_range + (-0.3, 3.6)*Volt).intersect(self.gnd.link().voltage + (-0.3, 5.5)*Volt) + tt_voltage_limit = input_range + (-0.3, 0.3) * Volt + io_voltage_limit = (input_range + (-0.3, 3.6) * Volt).intersect(self.gnd.link().voltage + (-0.3, 5.5) * Volt) dio_tta_model = DigitalBidir.from_supply( - self.gnd, self.pwr, + self.gnd, + self.pwr, voltage_limit_abs=tt_voltage_limit, - current_limits=(-20, 20)*mAmp, # Table 19 + current_limits=(-20, 20) * mAmp, # Table 19 input_threshold_factor=(0.3, 0.7), # section 6.3.14, simplest for 1.62 PinMapUtil: i2c_model = I2cController(DigitalBidir.empty()) i2c_target_model = I2cTarget(DigitalBidir.empty()) - return PinMapUtil([ # Table 12, partial table for up to 32-pin only - PinResource('PC14', {'PC14': dio_ft_model}), # OSC32_IN - PinResource('PC15', {'PC15': dio_ft_model}), # OSC32_OUT - PinResource('PA0', {'PA0': dio_fta_model, 'ADC1_IN5': adc_ft_model}), - PinResource('PA1', {'PA1': dio_fta_model, 'ADC_1IN6': adc_ft_model}), - PinResource('PA2', {'PA2': dio_fta_model, 'ADC_1IN7': adc_ft_model}), - PinResource('PA3', {'PA3': dio_tta_model, 'ADC_1IN8': adc_ft_model}), - PinResource('PA4', {'PA4': dio_tta_model, 'ADC_1IN9': adc_tt_model, 'DAC1_OUT1': dac_model}), - PinResource('PA5', {'PA5': dio_tta_model, 'ADC_1IN10': adc_tt_model, 'DAC1_OUT2': dac_model}), - PinResource('PA6', {'PA6': dio_fta_model, 'ADC_1IN10': adc_ft_model}), - - PinResource('PA7', {'PA7': dio_fta_model, 'ADC_1IN12': adc_ft_model}), - PinResource('PB0', {'PB0': dio_fta_model, 'ADC_1IN15': adc_ft_model}), - PinResource('PB1', {'PB1': dio_fta_model, 'ADC_1IN16': adc_ft_model}), - PinResource('PA8', {'PA8': dio_ft_model}), - PinResource('PA9', {'PA8': dio_ftf_model}), - PinResource('PA10', {'PA10': dio_ftf_model}), - PinResource('PA11', {'PA11': dio_ftu_model}), - PinResource('PA12', {'PA12': dio_ftu_model}), - PinResource('PA13', {'PA13': dio_ft_model}), - - PinResource('PA14', {'PA14': dio_ft_model}), - PinResource('PA15', {'PA15': dio_ft_model}), - PinResource('PB3', {'PB3': dio_fta_model}), - PinResource('PB4', {'PB4': dio_ftfa_model}), - PinResource('PB5', {'PB5': dio_ft_model}), - PinResource('PB6', {'PB6': dio_ftfa_model}), - PinResource('PB7', {'PB7': dio_ftfa_model}), - PinResource('PH3', {'PH3': dio_ft_model}), # BOOT0 - - PeripheralFixedResource('SPI1', spi_model, { - 'sck': ['PA1', 'PA5', 'PB3'], 'miso': ['PA6', 'PA11', 'PB4'], 'mosi': ['PA7', 'PA12', 'PB5'] - }), - PeripheralFixedResource('USART2', uart_model, { - 'tx': ['PA2'], 'rx': ['PA3', 'PA15'] - }), - PeripheralFixedResource('LPUART1', uart_model, { - 'tx': ['PA2'], 'rx': ['PA3'] - }), - PeripheralFixedResource('I2C3', i2c_model, { - 'scl': ['PA7'], 'sda': ['PB4'] - }), - PeripheralFixedResource('I2C3_T', i2c_target_model, { # TODO shared resource w/ I2C controller - 'scl': ['PA7'], 'sda': ['PB4'] - }), - PeripheralFixedResource('I2C1', i2c_model, { - 'scl': ['PA9', 'PB6'], 'sda': ['PA10', 'PB7'] - }), - PeripheralFixedResource('I2C1_T', i2c_target_model, { # TODO shared resource w/ I2C controller - 'scl': ['PA9', 'PB6'], 'sda': ['PA10', 'PB7'] - }), - PeripheralFixedResource('USART2', uart_model, { - 'tx': ['PA9'], 'rx': ['PA10'] - }), - PeripheralFixedResource('CAN', CanControllerPort(DigitalBidir.empty()), { - 'tx': ['PA12'], 'rx': ['PA11'] - }), - PeripheralFixedResource('USB', UsbDevicePort.empty(), { - 'dp': ['PA12'], 'dm': ['PA11'] - }), - PeripheralFixedResource('SPI3', spi_model, { - 'sck': ['PB3'], 'miso': ['PB4'], 'mosi': ['PB5'] - }), - PeripheralFixedResource('USART1', uart_model, { - 'tx': ['PB6'], 'rx': ['PB7'] - }), - - PeripheralFixedResource('SWD', SwdTargetPort(DigitalBidir.empty()), { - 'swdio': ['PA13'], 'swclk': ['PA14'], - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) + return PinMapUtil( + [ # Table 12, partial table for up to 32-pin only + PinResource("PC14", {"PC14": dio_ft_model}), # OSC32_IN + PinResource("PC15", {"PC15": dio_ft_model}), # OSC32_OUT + PinResource("PA0", {"PA0": dio_fta_model, "ADC1_IN5": adc_ft_model}), + PinResource("PA1", {"PA1": dio_fta_model, "ADC_1IN6": adc_ft_model}), + PinResource("PA2", {"PA2": dio_fta_model, "ADC_1IN7": adc_ft_model}), + PinResource("PA3", {"PA3": dio_tta_model, "ADC_1IN8": adc_ft_model}), + PinResource("PA4", {"PA4": dio_tta_model, "ADC_1IN9": adc_tt_model, "DAC1_OUT1": dac_model}), + PinResource("PA5", {"PA5": dio_tta_model, "ADC_1IN10": adc_tt_model, "DAC1_OUT2": dac_model}), + PinResource("PA6", {"PA6": dio_fta_model, "ADC_1IN10": adc_ft_model}), + PinResource("PA7", {"PA7": dio_fta_model, "ADC_1IN12": adc_ft_model}), + PinResource("PB0", {"PB0": dio_fta_model, "ADC_1IN15": adc_ft_model}), + PinResource("PB1", {"PB1": dio_fta_model, "ADC_1IN16": adc_ft_model}), + PinResource("PA8", {"PA8": dio_ft_model}), + PinResource("PA9", {"PA8": dio_ftf_model}), + PinResource("PA10", {"PA10": dio_ftf_model}), + PinResource("PA11", {"PA11": dio_ftu_model}), + PinResource("PA12", {"PA12": dio_ftu_model}), + PinResource("PA13", {"PA13": dio_ft_model}), + PinResource("PA14", {"PA14": dio_ft_model}), + PinResource("PA15", {"PA15": dio_ft_model}), + PinResource("PB3", {"PB3": dio_fta_model}), + PinResource("PB4", {"PB4": dio_ftfa_model}), + PinResource("PB5", {"PB5": dio_ft_model}), + PinResource("PB6", {"PB6": dio_ftfa_model}), + PinResource("PB7", {"PB7": dio_ftfa_model}), + PinResource("PH3", {"PH3": dio_ft_model}), # BOOT0 + PeripheralFixedResource( + "SPI1", + spi_model, + {"sck": ["PA1", "PA5", "PB3"], "miso": ["PA6", "PA11", "PB4"], "mosi": ["PA7", "PA12", "PB5"]}, + ), + PeripheralFixedResource("USART2", uart_model, {"tx": ["PA2"], "rx": ["PA3", "PA15"]}), + PeripheralFixedResource("LPUART1", uart_model, {"tx": ["PA2"], "rx": ["PA3"]}), + PeripheralFixedResource("I2C3", i2c_model, {"scl": ["PA7"], "sda": ["PB4"]}), + PeripheralFixedResource( + "I2C3_T", + i2c_target_model, + {"scl": ["PA7"], "sda": ["PB4"]}, # TODO shared resource w/ I2C controller + ), + PeripheralFixedResource("I2C1", i2c_model, {"scl": ["PA9", "PB6"], "sda": ["PA10", "PB7"]}), + PeripheralFixedResource( + "I2C1_T", + i2c_target_model, + {"scl": ["PA9", "PB6"], "sda": ["PA10", "PB7"]}, # TODO shared resource w/ I2C controller + ), + PeripheralFixedResource("USART2", uart_model, {"tx": ["PA9"], "rx": ["PA10"]}), + PeripheralFixedResource( + "CAN", CanControllerPort(DigitalBidir.empty()), {"tx": ["PA12"], "rx": ["PA11"]} + ), + PeripheralFixedResource("USB", UsbDevicePort.empty(), {"dp": ["PA12"], "dm": ["PA11"]}), + PeripheralFixedResource("SPI3", spi_model, {"sck": ["PB3"], "miso": ["PB4"], "mosi": ["PB5"]}), + PeripheralFixedResource("USART1", uart_model, {"tx": ["PB6"], "rx": ["PB7"]}), + PeripheralFixedResource( + "SWD", + SwdTargetPort(DigitalBidir.empty()), + { + "swdio": ["PA13"], + "swclk": ["PA14"], + }, + ), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) @override def generate(self) -> None: super().generate() self.footprint( - 'U', self.PACKAGE, + "U", + self.PACKAGE, self._make_pinning(), - mfr='STMicroelectronics', part=self.PART, - datasheet='https://www.st.com/resource/en/datasheet/stm32l432kc.pdf' + mfr="STMicroelectronics", + part=self.PART, + datasheet="https://www.st.com/resource/en/datasheet/stm32l432kc.pdf", ) self.assign(self.lcsc_part, self.LCSC_PART) self.assign(self.actual_basic_part, False) class Stm32l432k_Device(Stm32l432Base_Device): - """"STM32L432Kx in UFQFPN32 package.""" + """ "STM32L432Kx in UFQFPN32 package.""" + SYSTEM_PIN_REMAP = { - 'Vdd': ['17', '1'], - 'Vss': ['16', '32', '33'], # recommended to connect EP to PCB ground - 'VddA': '5', - 'NRST': '4', + "Vdd": ["17", "1"], + "Vss": ["16", "32", "33"], # recommended to connect EP to PCB ground + "VddA": "5", + "NRST": "4", } RESOURCE_PIN_REMAP = { - 'PC14': '2', - 'PC15': '3', - 'PA0': '6', - 'PA1': '7', - 'PA2': '8', - 'PA3': '9', - 'PA4': '10', - 'PA5': '11', - 'PA6': '12', - - 'PA7': '13', - 'PB0': '14', - 'PB1': '15', - 'PA8': '18', - 'PA9': '19', - 'PA10': '20', - 'PA11': '21', - 'PA12': '22', - 'PA13': '23', - - 'PA14': '24', - 'PA15': '25', - 'PB3': '26', - 'PB4': '27', - 'PB5': '28', - 'PB6': '29', - 'PB7': '30', - 'PH3': '31', + "PC14": "2", + "PC15": "3", + "PA0": "6", + "PA1": "7", + "PA2": "8", + "PA3": "9", + "PA4": "10", + "PA5": "11", + "PA6": "12", + "PA7": "13", + "PB0": "14", + "PB1": "15", + "PA8": "18", + "PA9": "19", + "PA10": "20", + "PA11": "21", + "PA12": "22", + "PA13": "23", + "PA14": "24", + "PA15": "25", + "PB3": "26", + "PB4": "27", + "PB5": "28", + "PB6": "29", + "PB7": "30", + "PH3": "31", } - PACKAGE = 'Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm' - PART = 'STM32L432Kxxx' - LCSC_PART = 'C1337280' # KCU6 variant, maximum memory variant + PACKAGE = "Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm" + PART = "STM32L432Kxxx" + LCSC_PART = "C1337280" # KCU6 variant, maximum memory variant @abstract_block -class Stm32l432Base(Resettable, IoControllerDac, IoControllerCan, IoControllerUsb, IoControllerI2cTarget, - Microcontroller, IoControllerWithSwdTargetConnector, WithCrystalGenerator, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): +class Stm32l432Base( + Resettable, + IoControllerDac, + IoControllerCan, + IoControllerUsb, + IoControllerI2cTarget, + Microcontroller, + IoControllerWithSwdTargetConnector, + WithCrystalGenerator, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): DEVICE: Type[Stm32l432Base_Device] = Stm32l432Base_Device def __init__(self, **kwargs: Any) -> None: @@ -233,21 +258,18 @@ def __init__(self, **kwargs: Any) -> None: def contents(self) -> None: super().contents() - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: self.ic = imp.Block(self.DEVICE(pin_assigns=ArrayStringExpr())) self.connect(self.swd_node, self.ic.swd) self.connect(self.reset_node, self.ic.nrst) # from datasheet power supply scheme - self.vdd_cap_bulk = imp.Block(DecouplingCapacitor(4.7*uFarad(tol=0.2))) + self.vdd_cap_bulk = imp.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))) self.vdd_cap = ElementDict[DecouplingCapacitor]() for i in range(2): # one for each Vdd/Vss pair - self.vdd_cap[i] = imp.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))) - self.vdda_cap0 = imp.Block(DecouplingCapacitor(10*nFarad(tol=0.2))) - self.vdda_cap1 = imp.Block(DecouplingCapacitor(1*uFarad(tol=0.2))) + self.vdd_cap[i] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.vdda_cap0 = imp.Block(DecouplingCapacitor(10 * nFarad(tol=0.2))) + self.vdda_cap1 = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))) @override def generate(self) -> None: diff --git a/edg/parts/Microcontroller_nRF52840.py b/edg/parts/Microcontroller_nRF52840.py index 94a5e0e78..afa9fcb82 100644 --- a/edg/parts/Microcontroller_nRF52840.py +++ b/edg/parts/Microcontroller_nRF52840.py @@ -8,521 +8,662 @@ @non_library -class Nrf52840_Interfaces(IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerUsb, IoControllerI2s, - IoControllerBle): - """Defines base interfaces for nRF52840 microcontrollers""" +class Nrf52840_Interfaces( + IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerUsb, IoControllerI2s, IoControllerBle +): + """Defines base interfaces for nRF52840 microcontrollers""" @non_library class Nrf52840_Ios(Nrf52840_Interfaces, BaseIoControllerPinmapGenerator, GeneratorBlock, FootprintBlock): - """nRF52840 IO mappings - https://infocenter.nordicsemi.com/pdf/nRF52840_PS_v1.7.pdf""" - RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name - - @abstractmethod - def _vddio(self) -> Port[VoltageLink]: - ... - - def _vdd_model(self) -> VoltageSink: - return VoltageSink( - voltage_limits=(1.75, 3.6)*Volt, # 1.75 minimum for power-on reset - current_draw=(0, 212 / 64 + 4.8)*mAmp + self.io_current_draw.upper() # CPU @ max 212 Coremarks + 4.8mA in RF transmit - ) - - def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: - return DigitalBidir.from_supply( - self.gnd, pwr, - voltage_limit_tolerance=(-0.3, 0.3) * Volt, - current_limits=(-6, 6)*mAmp, # minimum current, high drive, Vdd>2.7 - input_threshold_factor=(0.3, 0.7), - pullup_capable=True, pulldown_capable=True, - ) - - @override - def _io_pinmap(self) -> PinMapUtil: - """Returns the mappable for given the input power and ground references. - This separates the system pins definition from the IO pins definition.""" - pwr = self._vddio() - dio_model = self._dio_model(pwr) - dio_lf_model = dio_model # "standard drive, low frequency IO only" (differences not modeled) - - adc_model = AnalogSink.from_supply( - self.gnd, pwr, - voltage_limit_tolerance=(0, 0), # datasheet 6.23.2, analog inputs cannot exceed Vdd or be lower than Vss - signal_limit_tolerance=(0, 0), - impedance=Range.from_lower(1)*MOhm - ) - - uart_model = UartPort(DigitalBidir.empty()) - spi_model = SpiController(DigitalBidir.empty(), (125, 32000) * kHertz) - spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), (125, 32000) * kHertz) # tristated by CS pin - i2c_model = I2cController(DigitalBidir.empty()) - i2c_target_model = I2cTarget(DigitalBidir.empty()) - i2s_model = I2sController(DigitalBidir.empty()) - - hf_io_pins = [ - 'P0.00', 'P0.01', 'P0.26', 'P0.27', 'P0.04', - 'P0.05', 'P0.06', 'P0.07', 'P0.08', 'P1.08', 'P1.09', 'P0.11', 'P0.12', - 'P0.14', 'P0.16', 'P0.19', 'P0.21', 'P0.23', 'P0.25', # 'P0.18' - 'P0.13', 'P0.15', 'P0.17', 'P0.20', 'P0.22', 'P0.24', 'P1.00', - ] - - return PinMapUtil([ # Section 7.1.2 with QIAA aQFN73 & QFAA QFN48 pins only - PinResource('P0.31', {'P0.31': dio_lf_model, 'AIN7': adc_model}), - PinResource('P0.29', {'P0.29': dio_lf_model, 'AIN5': adc_model}), - PinResource('P0.02', {'P0.02': dio_lf_model, 'AIN0': adc_model}), - PinResource('P1.15', {'P1.15': dio_lf_model}), - PinResource('P1.13', {'P1.13': dio_lf_model}), - PinResource('P1.10', {'P1.10': dio_lf_model}), - PinResource('P0.30', {'P0.30': dio_lf_model, 'AIN6': adc_model}), - PinResource('P0.28', {'P0.28': dio_lf_model, 'AIN4': adc_model}), - PinResource('P0.03', {'P0.03': dio_lf_model, 'AIN1': adc_model}), - PinResource('P1.14', {'P1.14': dio_lf_model}), - PinResource('P1.12', {'P1.12': dio_lf_model}), - PinResource('P1.11', {'P1.11': dio_lf_model}), - PinResource('P0.00', {'P0.00': dio_model}), # TODO also 32.768 kHz crystal in - PinResource('P0.01', {'P0.01': dio_model}), # TODO also 32.768 kHz crystal in - PinResource('P0.26', {'P0.26': dio_model}), - PinResource('P0.27', {'P0.27': dio_model}), - PinResource('P0.04', {'P0.04': dio_model, 'AIN2': adc_model}), - PinResource('P0.10', {'P0.10': dio_lf_model}), # TODO also NFC2 - - PinResource('P0.05', {'P0.05': dio_model, 'AIN3': adc_model}), - PinResource('P0.06', {'P0.06': dio_model}), - PinResource('P0.09', {'P0.09': dio_lf_model}), # TODO also NFC1 - PinResource('P0.07', {'P0.07': dio_model}), - PinResource('P0.08', {'P0.08': dio_model}), - PinResource('P1.08', {'P1.08': dio_model}), - PinResource('P1.07', {'P1.07': dio_lf_model}), - PinResource('P1.09', {'P1.09': dio_model}), - PinResource('P1.06', {'P1.06': dio_lf_model}), - PinResource('P0.11', {'P0.11': dio_model}), - PinResource('P1.05', {'P1.05': dio_lf_model}), - PinResource('P0.12', {'P0.12': dio_model}), - PinResource('P1.04', {'P1.04': dio_lf_model}), - PinResource('P1.03', {'P1.03': dio_lf_model}), - PinResource('P1.02', {'P1.02': dio_lf_model}), - PinResource('P1.01', {'P1.01': dio_lf_model}), - PinResource('P0.14', {'P0.14': dio_model}), - PinResource('P0.16', {'P0.16': dio_model}), - # PinResource('P0.18', {'P0.18': dio_model}), # configurable as RESET, mappable - PinResource('P0.19', {'P0.19': dio_model}), - PinResource('P0.21', {'P0.21': dio_model}), - PinResource('P0.23', {'P0.23': dio_model}), - PinResource('P0.25', {'P0.25': dio_model}), - - PinResource('P0.13', {'P0.13': dio_model}), - PinResource('P0.15', {'P0.15': dio_model}), - PinResource('P0.17', {'P0.17': dio_model}), - PinResource('P0.20', {'P0.20': dio_model}), - PinResource('P0.22', {'P0.22': dio_model}), - PinResource('P0.24', {'P0.24': dio_model}), - PinResource('P1.00', {'P1.00': dio_model}), # TRACEDATA[0] and SWO, if used as IO must clear TRACECONFIG reg - - PeripheralFixedPin('SWD', SwdTargetPort(dio_model), { - 'swclk': 'SWCLK', 'swdio': 'SWDIO', - }), - PeripheralFixedPin('USBD', UsbDevicePort(), { - 'dp': 'D+', 'dm': 'D-' - }), - - PeripheralFixedResource('SPIM0', spi_model, { - 'sck': hf_io_pins, 'miso': hf_io_pins, 'mosi': hf_io_pins, - }), - PeripheralFixedResource('SPIM1', spi_model, { - 'sck': hf_io_pins, 'miso': hf_io_pins, 'mosi': hf_io_pins, - }), - PeripheralFixedResource('SPIM2', spi_model, { - 'sck': hf_io_pins, 'miso': hf_io_pins, 'mosi': hf_io_pins, - }), - PeripheralFixedResource('SPIM3', spi_model, { - 'sck': hf_io_pins, 'miso': hf_io_pins, 'mosi': hf_io_pins, - }), - PeripheralFixedResource('SPIS0', spi_peripheral_model, { # TODO shared resource w/ SPI controller - 'sck': hf_io_pins, 'miso': hf_io_pins, 'mosi': hf_io_pins, - }), - PeripheralFixedResource('SPIS1', spi_peripheral_model, { # TODO shared resource w/ SPI controller - 'sck': hf_io_pins, 'miso': hf_io_pins, 'mosi': hf_io_pins, - }), - PeripheralFixedResource('SPIS2', spi_peripheral_model, { # TODO shared resource w/ SPI controller - 'sck': hf_io_pins, 'miso': hf_io_pins, 'mosi': hf_io_pins, - }), - PeripheralFixedResource('TWIM0', i2c_model, { - 'scl': hf_io_pins, 'sda': hf_io_pins, - }), - PeripheralFixedResource('TWIM1', i2c_model, { - 'scl': hf_io_pins, 'sda': hf_io_pins, - }), - PeripheralFixedResource('TWIS0', i2c_target_model, { # TODO shared resource w/ I2C controller - 'scl': hf_io_pins, 'sda': hf_io_pins, - }), - PeripheralFixedResource('TWIS1', i2c_target_model, { # TODO shared resource w/ I2C controller - 'scl': hf_io_pins, 'sda': hf_io_pins, - }), - PeripheralFixedResource('UARTE0', uart_model, { - 'tx': hf_io_pins, 'rx': hf_io_pins, - }), - PeripheralFixedResource('UARTE1', uart_model, { - 'tx': hf_io_pins, 'rx': hf_io_pins, - }), - PeripheralFixedResource('I2S', i2s_model, { - 'sck': hf_io_pins, 'ws': hf_io_pins, 'sd': hf_io_pins, - }), - ]).remap_pins(self.RESOURCE_PIN_REMAP) + """nRF52840 IO mappings + https://infocenter.nordicsemi.com/pdf/nRF52840_PS_v1.7.pdf""" + + RESOURCE_PIN_REMAP: Dict[str, str] # resource name in base -> pin name + + @abstractmethod + def _vddio(self) -> Port[VoltageLink]: ... + + def _vdd_model(self) -> VoltageSink: + return VoltageSink( + voltage_limits=(1.75, 3.6) * Volt, # 1.75 minimum for power-on reset + current_draw=(0, 212 / 64 + 4.8) * mAmp + + self.io_current_draw.upper(), # CPU @ max 212 Coremarks + 4.8mA in RF transmit + ) + + def _dio_model(self, pwr: Port[VoltageLink]) -> DigitalBidir: + return DigitalBidir.from_supply( + self.gnd, + pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_limits=(-6, 6) * mAmp, # minimum current, high drive, Vdd>2.7 + input_threshold_factor=(0.3, 0.7), + pullup_capable=True, + pulldown_capable=True, + ) + + @override + def _io_pinmap(self) -> PinMapUtil: + """Returns the mappable for given the input power and ground references. + This separates the system pins definition from the IO pins definition.""" + pwr = self._vddio() + dio_model = self._dio_model(pwr) + dio_lf_model = dio_model # "standard drive, low frequency IO only" (differences not modeled) + + adc_model = AnalogSink.from_supply( + self.gnd, + pwr, + voltage_limit_tolerance=(0, 0), # datasheet 6.23.2, analog inputs cannot exceed Vdd or be lower than Vss + signal_limit_tolerance=(0, 0), + impedance=Range.from_lower(1) * MOhm, + ) + + uart_model = UartPort(DigitalBidir.empty()) + spi_model = SpiController(DigitalBidir.empty(), (125, 32000) * kHertz) + spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), (125, 32000) * kHertz) # tristated by CS pin + i2c_model = I2cController(DigitalBidir.empty()) + i2c_target_model = I2cTarget(DigitalBidir.empty()) + i2s_model = I2sController(DigitalBidir.empty()) + + hf_io_pins = [ + "P0.00", + "P0.01", + "P0.26", + "P0.27", + "P0.04", + "P0.05", + "P0.06", + "P0.07", + "P0.08", + "P1.08", + "P1.09", + "P0.11", + "P0.12", + "P0.14", + "P0.16", + "P0.19", + "P0.21", + "P0.23", + "P0.25", # 'P0.18' + "P0.13", + "P0.15", + "P0.17", + "P0.20", + "P0.22", + "P0.24", + "P1.00", + ] + + return PinMapUtil( + [ # Section 7.1.2 with QIAA aQFN73 & QFAA QFN48 pins only + PinResource("P0.31", {"P0.31": dio_lf_model, "AIN7": adc_model}), + PinResource("P0.29", {"P0.29": dio_lf_model, "AIN5": adc_model}), + PinResource("P0.02", {"P0.02": dio_lf_model, "AIN0": adc_model}), + PinResource("P1.15", {"P1.15": dio_lf_model}), + PinResource("P1.13", {"P1.13": dio_lf_model}), + PinResource("P1.10", {"P1.10": dio_lf_model}), + PinResource("P0.30", {"P0.30": dio_lf_model, "AIN6": adc_model}), + PinResource("P0.28", {"P0.28": dio_lf_model, "AIN4": adc_model}), + PinResource("P0.03", {"P0.03": dio_lf_model, "AIN1": adc_model}), + PinResource("P1.14", {"P1.14": dio_lf_model}), + PinResource("P1.12", {"P1.12": dio_lf_model}), + PinResource("P1.11", {"P1.11": dio_lf_model}), + PinResource("P0.00", {"P0.00": dio_model}), # TODO also 32.768 kHz crystal in + PinResource("P0.01", {"P0.01": dio_model}), # TODO also 32.768 kHz crystal in + PinResource("P0.26", {"P0.26": dio_model}), + PinResource("P0.27", {"P0.27": dio_model}), + PinResource("P0.04", {"P0.04": dio_model, "AIN2": adc_model}), + PinResource("P0.10", {"P0.10": dio_lf_model}), # TODO also NFC2 + PinResource("P0.05", {"P0.05": dio_model, "AIN3": adc_model}), + PinResource("P0.06", {"P0.06": dio_model}), + PinResource("P0.09", {"P0.09": dio_lf_model}), # TODO also NFC1 + PinResource("P0.07", {"P0.07": dio_model}), + PinResource("P0.08", {"P0.08": dio_model}), + PinResource("P1.08", {"P1.08": dio_model}), + PinResource("P1.07", {"P1.07": dio_lf_model}), + PinResource("P1.09", {"P1.09": dio_model}), + PinResource("P1.06", {"P1.06": dio_lf_model}), + PinResource("P0.11", {"P0.11": dio_model}), + PinResource("P1.05", {"P1.05": dio_lf_model}), + PinResource("P0.12", {"P0.12": dio_model}), + PinResource("P1.04", {"P1.04": dio_lf_model}), + PinResource("P1.03", {"P1.03": dio_lf_model}), + PinResource("P1.02", {"P1.02": dio_lf_model}), + PinResource("P1.01", {"P1.01": dio_lf_model}), + PinResource("P0.14", {"P0.14": dio_model}), + PinResource("P0.16", {"P0.16": dio_model}), + # PinResource('P0.18', {'P0.18': dio_model}), # configurable as RESET, mappable + PinResource("P0.19", {"P0.19": dio_model}), + PinResource("P0.21", {"P0.21": dio_model}), + PinResource("P0.23", {"P0.23": dio_model}), + PinResource("P0.25", {"P0.25": dio_model}), + PinResource("P0.13", {"P0.13": dio_model}), + PinResource("P0.15", {"P0.15": dio_model}), + PinResource("P0.17", {"P0.17": dio_model}), + PinResource("P0.20", {"P0.20": dio_model}), + PinResource("P0.22", {"P0.22": dio_model}), + PinResource("P0.24", {"P0.24": dio_model}), + PinResource( + "P1.00", {"P1.00": dio_model} + ), # TRACEDATA[0] and SWO, if used as IO must clear TRACECONFIG reg + PeripheralFixedPin( + "SWD", + SwdTargetPort(dio_model), + { + "swclk": "SWCLK", + "swdio": "SWDIO", + }, + ), + PeripheralFixedPin("USBD", UsbDevicePort(), {"dp": "D+", "dm": "D-"}), + PeripheralFixedResource( + "SPIM0", + spi_model, + { + "sck": hf_io_pins, + "miso": hf_io_pins, + "mosi": hf_io_pins, + }, + ), + PeripheralFixedResource( + "SPIM1", + spi_model, + { + "sck": hf_io_pins, + "miso": hf_io_pins, + "mosi": hf_io_pins, + }, + ), + PeripheralFixedResource( + "SPIM2", + spi_model, + { + "sck": hf_io_pins, + "miso": hf_io_pins, + "mosi": hf_io_pins, + }, + ), + PeripheralFixedResource( + "SPIM3", + spi_model, + { + "sck": hf_io_pins, + "miso": hf_io_pins, + "mosi": hf_io_pins, + }, + ), + PeripheralFixedResource( + "SPIS0", + spi_peripheral_model, + { # TODO shared resource w/ SPI controller + "sck": hf_io_pins, + "miso": hf_io_pins, + "mosi": hf_io_pins, + }, + ), + PeripheralFixedResource( + "SPIS1", + spi_peripheral_model, + { # TODO shared resource w/ SPI controller + "sck": hf_io_pins, + "miso": hf_io_pins, + "mosi": hf_io_pins, + }, + ), + PeripheralFixedResource( + "SPIS2", + spi_peripheral_model, + { # TODO shared resource w/ SPI controller + "sck": hf_io_pins, + "miso": hf_io_pins, + "mosi": hf_io_pins, + }, + ), + PeripheralFixedResource( + "TWIM0", + i2c_model, + { + "scl": hf_io_pins, + "sda": hf_io_pins, + }, + ), + PeripheralFixedResource( + "TWIM1", + i2c_model, + { + "scl": hf_io_pins, + "sda": hf_io_pins, + }, + ), + PeripheralFixedResource( + "TWIS0", + i2c_target_model, + { # TODO shared resource w/ I2C controller + "scl": hf_io_pins, + "sda": hf_io_pins, + }, + ), + PeripheralFixedResource( + "TWIS1", + i2c_target_model, + { # TODO shared resource w/ I2C controller + "scl": hf_io_pins, + "sda": hf_io_pins, + }, + ), + PeripheralFixedResource( + "UARTE0", + uart_model, + { + "tx": hf_io_pins, + "rx": hf_io_pins, + }, + ), + PeripheralFixedResource( + "UARTE1", + uart_model, + { + "tx": hf_io_pins, + "rx": hf_io_pins, + }, + ), + PeripheralFixedResource( + "I2S", + i2s_model, + { + "sck": hf_io_pins, + "ws": hf_io_pins, + "sd": hf_io_pins, + }, + ), + ] + ).remap_pins(self.RESOURCE_PIN_REMAP) @abstract_block class Nrf52840_Base(Nrf52840_Ios, GeneratorBlock): - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) - - @override - def _vddio(self) -> Port[VoltageLink]: - return self.pwr - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - return VariantPinRemapper({ - 'Vdd': self.pwr, - 'Vss': self.gnd, - 'Vbus': self.pwr_usb, - 'nRESET': self.nreset, - }).remap(self.SYSTEM_PIN_REMAP) - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.gnd = self.Port(Ground(), [Common]) - self.pwr = self.Port(self._vdd_model(), [Power]) - - self.pwr_usb = self.Port(VoltageSink( - voltage_limits=(4.35, 5.5)*Volt, - current_draw=(0.262, 7.73) * mAmp # CPU/USB sleeping to everything active - ), optional=True) - self.require((self.usb.length() > 0).implies(self.pwr_usb.is_connected()), "USB require Vbus connected") - - # Additional ports (on top of IoController) - # Crystals from table 15, 32, 33 - # TODO Table 32, model crystal load capacitance and series resistance ratings - self.xtal = self.Port(CrystalDriver(frequency_limits=(1, 25)*MHertz, voltage_out=self.pwr.link().voltage), - optional=True) - # Assumed from "32kHz crystal" in 14.5 - self.xtal_rtc = self.Port(CrystalDriver(frequency_limits=(32, 33)*kHertz, voltage_out=self.pwr.link().voltage), - optional=True) - - self.swd = self.Port(SwdTargetPort.empty()) - self.nreset = self.Port(DigitalSink.from_bidir(self._dio_model(self.pwr)), optional=True) - self._io_ports.insert(0, self.swd) + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] # pin name in base -> pin name(s) + + @override + def _vddio(self) -> Port[VoltageLink]: + return self.pwr + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + return VariantPinRemapper( + { + "Vdd": self.pwr, + "Vss": self.gnd, + "Vbus": self.pwr_usb, + "nRESET": self.nreset, + } + ).remap(self.SYSTEM_PIN_REMAP) + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.gnd = self.Port(Ground(), [Common]) + self.pwr = self.Port(self._vdd_model(), [Power]) + + self.pwr_usb = self.Port( + VoltageSink( + voltage_limits=(4.35, 5.5) * Volt, + current_draw=(0.262, 7.73) * mAmp, # CPU/USB sleeping to everything active + ), + optional=True, + ) + self.require((self.usb.length() > 0).implies(self.pwr_usb.is_connected()), "USB require Vbus connected") + + # Additional ports (on top of IoController) + # Crystals from table 15, 32, 33 + # TODO Table 32, model crystal load capacitance and series resistance ratings + self.xtal = self.Port( + CrystalDriver(frequency_limits=(1, 25) * MHertz, voltage_out=self.pwr.link().voltage), optional=True + ) + # Assumed from "32kHz crystal" in 14.5 + self.xtal_rtc = self.Port( + CrystalDriver(frequency_limits=(32, 33) * kHertz, voltage_out=self.pwr.link().voltage), optional=True + ) + + self.swd = self.Port(SwdTargetPort.empty()) + self.nreset = self.Port(DigitalSink.from_bidir(self._dio_model(self.pwr)), optional=True) + self._io_ports.insert(0, self.swd) class Holyiot_18010_Device(Nrf52840_Base, InternalSubcircuit): - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'Vdd': '14', - 'Vss': ['1', '25', '37'], - 'Vbus': '22', - 'nRESET': '21', - } - RESOURCE_PIN_REMAP = { # boundary pins only, inner pins ignored - 'P1.11': '2', - 'P1.10': '3', - 'P1.13': '4', - 'P1.15': '5', - 'P0.03': '6', - 'P0.02': '7', - 'P0.28': '8', - 'P0.29': '9', - 'P0.30': '10', - 'P0.31': '11', - 'P0.04': '12', - 'P0.05': '13', - - 'P0.07': '15', - 'P1.09': '16', - 'P0.12': '17', - 'P0.23': '18', - 'P0.21': '19', - 'P0.19': '20', - 'D-': '23', - 'D+': '24', - - 'P0.22': '26', - 'P1.00': '27', - 'P1.03': '28', - 'P1.01': '29', - 'P1.02': '30', - 'SWCLK': '31', - 'SWDIO': '32', - 'P1.04': '33', - 'P1.06': '34', - 'P0.09': '35', - 'P0.10': '36', - } - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', 'edg:Holyiot-18010-NRF52840', - self._make_pinning(), - mfr='Holyiot', part='18010', - datasheet='http://www.holyiot.com/tp/2019042516322180424.pdf' - ) - - -class Holyiot_18010(Microcontroller, Radiofrequency, Resettable, Nrf52840_Interfaces, IoControllerWithSwdTargetConnector, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): - """Wrapper around the Holyiot 18010 that includes supporting components (programming port)""" - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.ic: Holyiot_18010_Device - self.ic = self.Block(Holyiot_18010_Device(pin_assigns=ArrayStringExpr())) - self.pwr_usb = self.Export(self.ic.pwr_usb, optional=True) - self.generator_param(self.reset.is_connected()) - - @override - def contents(self) -> None: - super().contents() - self.connect(self.pwr, self.ic.pwr) - self.connect(self.gnd, self.ic.gnd) - - self.connect(self.swd_node, self.ic.swd) - self.connect(self.reset_node, self.ic.nreset) - - @override - def generate(self) -> None: - super().generate() - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.nreset) + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "Vdd": "14", + "Vss": ["1", "25", "37"], + "Vbus": "22", + "nRESET": "21", + } + RESOURCE_PIN_REMAP = { # boundary pins only, inner pins ignored + "P1.11": "2", + "P1.10": "3", + "P1.13": "4", + "P1.15": "5", + "P0.03": "6", + "P0.02": "7", + "P0.28": "8", + "P0.29": "9", + "P0.30": "10", + "P0.31": "11", + "P0.04": "12", + "P0.05": "13", + "P0.07": "15", + "P1.09": "16", + "P0.12": "17", + "P0.23": "18", + "P0.21": "19", + "P0.19": "20", + "D-": "23", + "D+": "24", + "P0.22": "26", + "P1.00": "27", + "P1.03": "28", + "P1.01": "29", + "P1.02": "30", + "SWCLK": "31", + "SWDIO": "32", + "P1.04": "33", + "P1.06": "34", + "P0.09": "35", + "P0.10": "36", + } + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + "edg:Holyiot-18010-NRF52840", + self._make_pinning(), + mfr="Holyiot", + part="18010", + datasheet="http://www.holyiot.com/tp/2019042516322180424.pdf", + ) + + +class Holyiot_18010( + Microcontroller, + Radiofrequency, + Resettable, + Nrf52840_Interfaces, + IoControllerWithSwdTargetConnector, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): + """Wrapper around the Holyiot 18010 that includes supporting components (programming port)""" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.ic: Holyiot_18010_Device + self.ic = self.Block(Holyiot_18010_Device(pin_assigns=ArrayStringExpr())) + self.pwr_usb = self.Export(self.ic.pwr_usb, optional=True) + self.generator_param(self.reset.is_connected()) + + @override + def contents(self) -> None: + super().contents() + self.connect(self.pwr, self.ic.pwr) + self.connect(self.gnd, self.ic.gnd) + + self.connect(self.swd_node, self.ic.swd) + self.connect(self.reset_node, self.ic.nreset) + + @override + def generate(self) -> None: + super().generate() + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.nreset) + class Mdbt50q_1mv2_Device(Nrf52840_Base, InternalSubcircuit, JlcPart): - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'Vdd': ['28', '30'], # 28=Vdd, 30=VddH; 31=DccH is disconnected - from section 8.3 for input voltage <3.6v - 'Vss': ['1', '2', '15', '33', '55'], - 'Vbus': '32', - 'nRESET': '40', - } - RESOURCE_PIN_REMAP = { # boundary pins only, inner pins ignored - 'P1.10': '3', - 'P1.11': '4', - 'P1.12': '5', - 'P1.13': '6', - 'P1.14': '7', - 'P1.15': '8', - 'P0.03': '9', - 'P0.29': '10', - 'P0.02': '11', - 'P0.31': '12', - 'P0.28': '13', - 'P0.30': '14', - - 'P0.27': '16', - 'P0.00': '17', - 'P0.01': '18', - 'P0.26': '19', - 'P0.04': '20', - 'P0.05': '21', - 'P0.06': '22', - 'P0.07': '23', - 'P0.08': '24', - 'P1.08': '25', - 'P1.09': '26', - 'P0.11': '27', - 'P0.12': '29', - 'D-': '34', - 'D+': '35', - - 'P0.14': '36', - 'P0.13': '37', - 'P0.16': '38', - 'P0.15': '39', - 'P0.17': '41', - 'P0.19': '42', - 'P0.21': '43', - 'P0.20': '44', - 'P0.23': '45', - 'P0.22': '46', - 'P1.00': '47', - 'P0.24': '48', - 'P0.25': '49', - 'P1.02': '50', - 'SWDIO': '51', - 'P0.09': '52', - 'SWCLK': '53', - 'P0.10': '54', - - 'P1.04': '56', - 'P1.06': '57', - 'P1.07': '58', - 'P1.05': '59', - 'P1.03': '60', - 'P1.01': '61', - } - - @override - def generate(self) -> None: - super().generate() - - self.assign(self.lcsc_part, 'C5118826') - self.assign(self.actual_basic_part, False) - self.footprint( - 'U', 'RF_Module:Raytac_MDBT50Q', - self._make_pinning(), - mfr='Raytac', part='MDBT50Q-1MV2', - datasheet='https://www.raytac.com/download/index.php?index_id=43' - ) + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "Vdd": ["28", "30"], # 28=Vdd, 30=VddH; 31=DccH is disconnected - from section 8.3 for input voltage <3.6v + "Vss": ["1", "2", "15", "33", "55"], + "Vbus": "32", + "nRESET": "40", + } + RESOURCE_PIN_REMAP = { # boundary pins only, inner pins ignored + "P1.10": "3", + "P1.11": "4", + "P1.12": "5", + "P1.13": "6", + "P1.14": "7", + "P1.15": "8", + "P0.03": "9", + "P0.29": "10", + "P0.02": "11", + "P0.31": "12", + "P0.28": "13", + "P0.30": "14", + "P0.27": "16", + "P0.00": "17", + "P0.01": "18", + "P0.26": "19", + "P0.04": "20", + "P0.05": "21", + "P0.06": "22", + "P0.07": "23", + "P0.08": "24", + "P1.08": "25", + "P1.09": "26", + "P0.11": "27", + "P0.12": "29", + "D-": "34", + "D+": "35", + "P0.14": "36", + "P0.13": "37", + "P0.16": "38", + "P0.15": "39", + "P0.17": "41", + "P0.19": "42", + "P0.21": "43", + "P0.20": "44", + "P0.23": "45", + "P0.22": "46", + "P1.00": "47", + "P0.24": "48", + "P0.25": "49", + "P1.02": "50", + "SWDIO": "51", + "P0.09": "52", + "SWCLK": "53", + "P0.10": "54", + "P1.04": "56", + "P1.06": "57", + "P1.07": "58", + "P1.05": "59", + "P1.03": "60", + "P1.01": "61", + } + + @override + def generate(self) -> None: + super().generate() + + self.assign(self.lcsc_part, "C5118826") + self.assign(self.actual_basic_part, False) + self.footprint( + "U", + "RF_Module:Raytac_MDBT50Q", + self._make_pinning(), + mfr="Raytac", + part="MDBT50Q-1MV2", + datasheet="https://www.raytac.com/download/index.php?index_id=43", + ) class Mdbt50q_UsbSeriesResistor(InternalSubcircuit, Block): - def __init__(self) -> None: - super().__init__() - self.usb_inner = self.Port(UsbHostPort.empty(), [Input]) - self.usb_outer = self.Port(UsbDevicePort.empty(), [Output]) - self.res_dp = self.Block(Resistor(27*Ohm(tol=0.01))) - self.res_dm = self.Block(Resistor(27*Ohm(tol=0.01))) - self.connect(self.usb_inner.dp, self.res_dp.a.adapt_to(DigitalBidir())) # TODO propagate params - needs bridge mechanism - self.connect(self.usb_outer.dp, self.res_dp.b.adapt_to(DigitalBidir())) - self.connect(self.usb_inner.dm, self.res_dm.a.adapt_to(DigitalBidir())) - self.connect(self.usb_outer.dm, self.res_dm.b.adapt_to(DigitalBidir())) - - -class Mdbt50q_1mv2(Microcontroller, Radiofrequency, Resettable, Nrf52840_Interfaces, IoControllerWithSwdTargetConnector, - IoControllerPowerRequired, BaseIoControllerExportable, GeneratorBlock): - """Wrapper around the Mdbt50q_1mv2 that includes the reference schematic""" - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.ic: Mdbt50q_1mv2_Device - self.ic = self.Block(Mdbt50q_1mv2_Device(pin_assigns=ArrayStringExpr())) # defined in generator to mix in SWO/TDI - self.pwr_usb = self.Export(self.ic.pwr_usb, optional=True) - self.generator_param(self.reset.is_connected()) - - @override - def contents(self) -> None: - super().contents() - self.connect(self.pwr, self.ic.pwr) - self.connect(self.gnd, self.ic.gnd) - - self.connect(self.swd_node, self.ic.swd) - self.connect(self.reset_node, self.ic.nreset) - - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.vcc_cap = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) - - @override - def generate(self) -> None: - super().generate() - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.nreset) - - ExportType = TypeVar('ExportType', bound=Port) - @override - def _make_export_vector(self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, - assign: Optional[str]) -> Optional[str]: - if isinstance(self_io, UsbDevicePort): # assumed at most one USB port generates - inner_io = inner_vector.request(name) - (self.usb_res, ), self.usb_chain = self.chain(inner_io, self.Block(Mdbt50q_UsbSeriesResistor()), self_io) - self.vbus_cap = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr_usb) - return assign - return super()._make_export_vector(self_io, inner_vector, name, assign) - - -class Feather_Nrf52840(IoControllerUsbOut, IoControllerPowerOut, Nrf52840_Ios, IoController, GeneratorBlock, - FootprintBlock): - """Feather nRF52840 socketed dev board as either power source or sink""" - - SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { - 'Vdd': '2', # 3v3 - 'Vss': '4', - - # 'reset': '1', - 'Vbus': '26', - # 'EN': '27', # controls the onboard 3.3 LDO, internally pulled up - # 'Vbat': '28', - } - RESOURCE_PIN_REMAP = { # boundary pins only, inner pins ignored - 'P0.31': '3', # AREF - 'P0.04': '5', # A0 - 'P0.05': '6', # A1 - 'P0.30': '7', # A2 - 'P0.28': '8', # A3 - 'P0.02': '9', # A4 - 'P0.03': '10', # A5 - 'P0.14': '11', # SCK - 'P0.13': '12', # MOSI - 'P0.15': '13', # MISO - 'P0.24': '14', # RXD - 'P0.25': '15', # TXD - 'P0.10': '16', # D2 - - 'P0.12': '17', # SDA - 'P0.11': '18', # SCL - 'P1.08': '19', # D5 - 'P0.07': '20', # D6 - 'P0.26': '21', # D9 - 'P0.27': '22', # D10 - 'P0.06': '23', # D11 - 'P0.08': '24', # D12 - 'P1.09': '25', # D13 - - # note onboard LED1 at P1.15, LED2 at P1.10 - # note onboard switch at P1.02, reset switch at P0.18 - # note onboard neopixel at P0.16 (data out not broken out) - # note onboard VBAT sense divider at P0.29 - } - - @override - def _vddio(self) -> Port[VoltageLink]: - if self.get(self.pwr.is_connected()): # board sinks power - return self.pwr - else: - return self.pwr_out - - @override - def _system_pinmap(self) -> Dict[str, CircuitPort]: - if self.get(self.pwr.is_connected()): # board sinks power - self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") - self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") - return VariantPinRemapper({ - 'Vdd': self.pwr, - 'Vss': self.gnd, - }).remap(self.SYSTEM_PIN_REMAP) - else: # board sources power (default) - return VariantPinRemapper({ - 'Vdd': self.pwr_out, - 'Vss': self.gnd, - 'Vbus': self.vusb_out, - }).remap(self.SYSTEM_PIN_REMAP) - - @override - def contents(self) -> None: - super().contents() - - self.gnd.init_from(Ground()) - self.pwr.init_from(self._vdd_model()) - - mbr120_drop = (0, 0.340)*Volt - ap2112_3v3_out = 3.3*Volt(tol=0.015) # note dropout voltage up to 400mV, current up to 600mA - self.vusb_out.init_from(VoltageSource( - voltage_out=UsbConnector.USB2_VOLTAGE_RANGE - mbr120_drop, - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - self.pwr_out.init_from(VoltageSource( - voltage_out=ap2112_3v3_out, - current_limits=UsbConnector.USB2_CURRENT_LIMITS - )) - - self.generator_param(self.pwr.is_connected()) - - @override - def generate(self) -> None: - super().generate() - - self.footprint( - 'U', 'bldc:FEATHERWING_NODIM', - self._make_pinning(), - mfr='Adafruit', part='Feather nRF52840 Express', - datasheet='https://learn.adafruit.com/assets/68545' - ) + def __init__(self) -> None: + super().__init__() + self.usb_inner = self.Port(UsbHostPort.empty(), [Input]) + self.usb_outer = self.Port(UsbDevicePort.empty(), [Output]) + self.res_dp = self.Block(Resistor(27 * Ohm(tol=0.01))) + self.res_dm = self.Block(Resistor(27 * Ohm(tol=0.01))) + self.connect( + self.usb_inner.dp, self.res_dp.a.adapt_to(DigitalBidir()) + ) # TODO propagate params - needs bridge mechanism + self.connect(self.usb_outer.dp, self.res_dp.b.adapt_to(DigitalBidir())) + self.connect(self.usb_inner.dm, self.res_dm.a.adapt_to(DigitalBidir())) + self.connect(self.usb_outer.dm, self.res_dm.b.adapt_to(DigitalBidir())) + + +class Mdbt50q_1mv2( + Microcontroller, + Radiofrequency, + Resettable, + Nrf52840_Interfaces, + IoControllerWithSwdTargetConnector, + IoControllerPowerRequired, + BaseIoControllerExportable, + GeneratorBlock, +): + """Wrapper around the Mdbt50q_1mv2 that includes the reference schematic""" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.ic: Mdbt50q_1mv2_Device + self.ic = self.Block( + Mdbt50q_1mv2_Device(pin_assigns=ArrayStringExpr()) + ) # defined in generator to mix in SWO/TDI + self.pwr_usb = self.Export(self.ic.pwr_usb, optional=True) + self.generator_param(self.reset.is_connected()) + + @override + def contents(self) -> None: + super().contents() + self.connect(self.pwr, self.ic.pwr) + self.connect(self.gnd, self.ic.gnd) + + self.connect(self.swd_node, self.ic.swd) + self.connect(self.reset_node, self.ic.nreset) + + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.vcc_cap = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) + + @override + def generate(self) -> None: + super().generate() + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.nreset) + + ExportType = TypeVar("ExportType", bound=Port) + + @override + def _make_export_vector( + self, self_io: ExportType, inner_vector: Vector[ExportType], name: str, assign: Optional[str] + ) -> Optional[str]: + if isinstance(self_io, UsbDevicePort): # assumed at most one USB port generates + inner_io = inner_vector.request(name) + (self.usb_res,), self.usb_chain = self.chain(inner_io, self.Block(Mdbt50q_UsbSeriesResistor()), self_io) + self.vbus_cap = self.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))).connected(self.gnd, self.pwr_usb) + return assign + return super()._make_export_vector(self_io, inner_vector, name, assign) + + +class Feather_Nrf52840( + IoControllerUsbOut, IoControllerPowerOut, Nrf52840_Ios, IoController, GeneratorBlock, FootprintBlock +): + """Feather nRF52840 socketed dev board as either power source or sink""" + + SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = { + "Vdd": "2", # 3v3 + "Vss": "4", + # 'reset': '1', + "Vbus": "26", + # 'EN': '27', # controls the onboard 3.3 LDO, internally pulled up + # 'Vbat': '28', + } + RESOURCE_PIN_REMAP = { # boundary pins only, inner pins ignored + "P0.31": "3", # AREF + "P0.04": "5", # A0 + "P0.05": "6", # A1 + "P0.30": "7", # A2 + "P0.28": "8", # A3 + "P0.02": "9", # A4 + "P0.03": "10", # A5 + "P0.14": "11", # SCK + "P0.13": "12", # MOSI + "P0.15": "13", # MISO + "P0.24": "14", # RXD + "P0.25": "15", # TXD + "P0.10": "16", # D2 + "P0.12": "17", # SDA + "P0.11": "18", # SCL + "P1.08": "19", # D5 + "P0.07": "20", # D6 + "P0.26": "21", # D9 + "P0.27": "22", # D10 + "P0.06": "23", # D11 + "P0.08": "24", # D12 + "P1.09": "25", # D13 + # note onboard LED1 at P1.15, LED2 at P1.10 + # note onboard switch at P1.02, reset switch at P0.18 + # note onboard neopixel at P0.16 (data out not broken out) + # note onboard VBAT sense divider at P0.29 + } + + @override + def _vddio(self) -> Port[VoltageLink]: + if self.get(self.pwr.is_connected()): # board sinks power + return self.pwr + else: + return self.pwr_out + + @override + def _system_pinmap(self) -> Dict[str, CircuitPort]: + if self.get(self.pwr.is_connected()): # board sinks power + self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected") + self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected") + return VariantPinRemapper( + { + "Vdd": self.pwr, + "Vss": self.gnd, + } + ).remap(self.SYSTEM_PIN_REMAP) + else: # board sources power (default) + return VariantPinRemapper( + { + "Vdd": self.pwr_out, + "Vss": self.gnd, + "Vbus": self.vusb_out, + } + ).remap(self.SYSTEM_PIN_REMAP) + + @override + def contents(self) -> None: + super().contents() + + self.gnd.init_from(Ground()) + self.pwr.init_from(self._vdd_model()) + + mbr120_drop = (0, 0.340) * Volt + ap2112_3v3_out = 3.3 * Volt(tol=0.015) # note dropout voltage up to 400mV, current up to 600mA + self.vusb_out.init_from( + VoltageSource( + voltage_out=UsbConnector.USB2_VOLTAGE_RANGE - mbr120_drop, + current_limits=UsbConnector.USB2_CURRENT_LIMITS, + ) + ) + self.pwr_out.init_from( + VoltageSource(voltage_out=ap2112_3v3_out, current_limits=UsbConnector.USB2_CURRENT_LIMITS) + ) + + self.generator_param(self.pwr.is_connected()) + + @override + def generate(self) -> None: + super().generate() + + self.footprint( + "U", + "bldc:FEATHERWING_NODIM", + self._make_pinning(), + mfr="Adafruit", + part="Feather nRF52840 Express", + datasheet="https://learn.adafruit.com/assets/68545", + ) diff --git a/edg/parts/Microphone_Sd18ob261.py b/edg/parts/Microphone_Sd18ob261.py index a092bf270..fbf27bc1e 100644 --- a/edg/parts/Microphone_Sd18ob261.py +++ b/edg/parts/Microphone_Sd18ob261.py @@ -8,42 +8,49 @@ class Sd18ob261_Device(InternalSubcircuit, JlcPart, FootprintBlock): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(1.6, 3.6) * Volt, - current_draw=(10, 350) * uAmp, # sleep current to max consumption - ), [Power]) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(1.6, 3.6) * Volt, + current_draw=(10, 350) * uAmp, # sleep current to max consumption + ), + [Power], + ) self.gnd = self.Port(Ground(), [Common]) - self.clk = self.Port(DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_abs=(-0.3, 3.6), - input_threshold_factor=(0.35, 0.65) - )) - self.lr = self.Port(DigitalSink( # select pin - voltage_limits=(-0.3, 3.6)*Volt, - input_thresholds=(0.2, self.vdd.link().voltage.lower() - 0.45) - )) - - self.data = self.Port(DigitalSource.from_supply( - self.gnd, self.vdd, - current_limits=(-20, 20)*mAmp # short circuit current for data pin - )) + self.clk = self.Port( + DigitalSink.from_supply( + self.gnd, self.vdd, voltage_limit_abs=(-0.3, 3.6), input_threshold_factor=(0.35, 0.65) + ) + ) + self.lr = self.Port( + DigitalSink( # select pin + voltage_limits=(-0.3, 3.6) * Volt, input_thresholds=(0.2, self.vdd.link().voltage.lower() - 0.45) + ) + ) + + self.data = self.Port( + DigitalSource.from_supply( + self.gnd, self.vdd, current_limits=(-20, 20) * mAmp # short circuit current for data pin + ) + ) @override def contents(self) -> None: self.footprint( - 'U', 'Sensor_Audio:Knowles_LGA-5_3.5x2.65mm', + "U", + "Sensor_Audio:Knowles_LGA-5_3.5x2.65mm", { - '1': self.data, - '2': self.lr, # 0 for data valid on CLK low, 1 for data valid on CLK high - '3': self.gnd, - '4': self.clk, - '5': self.vdd, + "1": self.data, + "2": self.lr, # 0 for data valid on CLK low, 1 for data valid on CLK high + "3": self.gnd, + "4": self.clk, + "5": self.vdd, }, - mfr='Goertek', part='SD18OB261-060', - datasheet='https://datasheet.lcsc.com/lcsc/2208241200_Goertek-SD18OB261-060_C2895290.pdf' + mfr="Goertek", + part="SD18OB261-060", + datasheet="https://datasheet.lcsc.com/lcsc/2208241200_Goertek-SD18OB261-060_C2895290.pdf", ) - self.assign(self.lcsc_part, 'C2895290') + self.assign(self.lcsc_part, "C2895290") self.assign(self.actual_basic_part, False) @@ -51,6 +58,7 @@ class Sd18ob261(Microphone, GeneratorBlock): """SD18OB261-060 PDM microphone, probably footprint-compatible with similar Knowles devices. Application circuit is not specified in the datasheet, this uses the one from SPH0655LM4H (single 0.1uF decap).""" + def __init__(self) -> None: super().__init__() @@ -68,7 +76,7 @@ def __init__(self) -> None: def generate(self) -> None: super().generate() - self.pwr_cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.pwr_cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) if self.get(self.lr.is_connected()): self.connect(self.lr, self.ic.lr) diff --git a/edg/parts/MotorDriver_Drv8833.py b/edg/parts/MotorDriver_Drv8833.py index 422b58b91..8c99ff2ad 100644 --- a/edg/parts/MotorDriver_Drv8833.py +++ b/edg/parts/MotorDriver_Drv8833.py @@ -5,121 +5,134 @@ class Drv8833_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.vm = self.Port(VoltageSink( - voltage_limits=(2.7, 10.8)*Volt, current_draw=RangeExpr()) - ) - self.gnd = self.Port(Ground()) - self.vint = self.Port(VoltageSource( # internal supply bypass - voltage_out=(0, 6.3)*Volt, # inferred from capacitor rating, actual voltage likely lower - current_limits=0*mAmp(tol=0) # external draw not allowed - )) - self.vcp = self.Port(Passive()) + def __init__(self) -> None: + super().__init__() + self.vm = self.Port(VoltageSink(voltage_limits=(2.7, 10.8) * Volt, current_draw=RangeExpr())) + self.gnd = self.Port(Ground()) + self.vint = self.Port( + VoltageSource( # internal supply bypass + voltage_out=(0, 6.3) * Volt, # inferred from capacitor rating, actual voltage likely lower + current_limits=0 * mAmp(tol=0), # external draw not allowed + ) + ) + self.vcp = self.Port(Passive()) - din_model = DigitalSink( # all pins pulled down by default - voltage_limits=(-0.3, 5.75)*Volt, - input_thresholds=(0.7, 2) - ) - self.ain1 = self.Port(din_model, optional=True) - self.ain2 = self.Port(din_model, optional=True) - self.bin1 = self.Port(din_model, optional=True) - self.bin2 = self.Port(din_model, optional=True) - self.nsleep = self.Port(DigitalSink( # internally pulled down (sleep mode) - voltage_limits=(-0.3, 5.75)*Volt, - input_thresholds=(0.5, 2.5) # thresholds different than rest of digital inputs - )) + din_model = DigitalSink( # all pins pulled down by default + voltage_limits=(-0.3, 5.75) * Volt, input_thresholds=(0.7, 2) + ) + self.ain1 = self.Port(din_model, optional=True) + self.ain2 = self.Port(din_model, optional=True) + self.bin1 = self.Port(din_model, optional=True) + self.bin2 = self.Port(din_model, optional=True) + self.nsleep = self.Port( + DigitalSink( # internally pulled down (sleep mode) + voltage_limits=(-0.3, 5.75) * Volt, + input_thresholds=(0.5, 2.5), # thresholds different than rest of digital inputs + ) + ) - dout_model = DigitalSource.from_supply(self.gnd, self.vm, current_limits=(-1.5, 1.5)*Amp) - self.aout1 = self.Port(dout_model, optional=True) - self.aout2 = self.Port(dout_model, optional=True) - self.bout1 = self.Port(dout_model, optional=True) - self.bout2 = self.Port(dout_model, optional=True) + dout_model = DigitalSource.from_supply(self.gnd, self.vm, current_limits=(-1.5, 1.5) * Amp) + self.aout1 = self.Port(dout_model, optional=True) + self.aout2 = self.Port(dout_model, optional=True) + self.bout1 = self.Port(dout_model, optional=True) + self.bout2 = self.Port(dout_model, optional=True) - @override - def contents(self) -> None: - self.assign(self.vm.current_draw, (1.6, 3000) * uAmp + # from sleep to max operating - (0, # calculate possible motor current, assuming A1/2 and B1/2 are coupled (and not independent) - self.aout1.is_connected().then_else(self.aout1.link().current_drawn.abs().upper(), 0*mAmp).max( - self.aout2.is_connected().then_else(self.aout2.link().current_drawn.abs().upper(), 0*mAmp)) + - self.bout1.is_connected().then_else(self.bout1.link().current_drawn.abs().upper(), 0*mAmp).max( - self.bout2.is_connected().then_else(self.bout2.link().current_drawn.abs().upper(), 0*mAmp)) - )) + @override + def contents(self) -> None: + self.assign( + self.vm.current_draw, + (1.6, 3000) * uAmp # from sleep to max operating + + ( + 0, # calculate possible motor current, assuming A1/2 and B1/2 are coupled (and not independent) + self.aout1.is_connected() + .then_else(self.aout1.link().current_drawn.abs().upper(), 0 * mAmp) + .max(self.aout2.is_connected().then_else(self.aout2.link().current_drawn.abs().upper(), 0 * mAmp)) + + self.bout1.is_connected() + .then_else(self.bout1.link().current_drawn.abs().upper(), 0 * mAmp) + .max(self.bout2.is_connected().then_else(self.bout2.link().current_drawn.abs().upper(), 0 * mAmp)), + ), + ) - self.require(self.aout1.is_connected() | self.aout2.is_connected() | - self.bout1.is_connected() | self.bout2.is_connected()) - self.require(self.aout1.is_connected().implies(self.ain1.is_connected())) - self.require(self.aout2.is_connected().implies(self.ain2.is_connected())) - self.require(self.bout1.is_connected().implies(self.bin1.is_connected())) - self.require(self.bout2.is_connected().implies(self.bin2.is_connected())) + self.require( + self.aout1.is_connected() + | self.aout2.is_connected() + | self.bout1.is_connected() + | self.bout2.is_connected() + ) + self.require(self.aout1.is_connected().implies(self.ain1.is_connected())) + self.require(self.aout2.is_connected().implies(self.ain2.is_connected())) + self.require(self.bout1.is_connected().implies(self.bin1.is_connected())) + self.require(self.bout2.is_connected().implies(self.bin2.is_connected())) - self.footprint( - 'U', 'Package_SO:TSSOP-16-1EP_4.4x5mm_P0.65mm_EP3x3mm_ThermalVias', - # note: the above has 0.3mm thermal vias while - # Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm_ThermalVias - # has 0.2mm which is below minimums for some fabs - { - '1': self.nsleep, - '2': self.aout1, - '3': self.gnd, # AISEN - '4': self.aout2, - '5': self.bout2, - '6': self.gnd, # BISEN - '7': self.bout1, - # '8': self.nfault, # TODO at some point - '9': self.bin1, - '10': self.bin2, - '11': self.vcp, - '12': self.vm, - '13': self.gnd, - '14': self.vint, - '15': self.ain2, - '16': self.ain1, - '17': self.gnd, # exposed pad - }, - mfr='Texas Instruments', part='DRV8833PWP', # also compatible w/ PW package (no GND pad) - datasheet='https://www.ti.com/lit/ds/symlink/drv8833.pdf' - ) - self.assign(self.lcsc_part, 'C50506') + self.footprint( + "U", + "Package_SO:TSSOP-16-1EP_4.4x5mm_P0.65mm_EP3x3mm_ThermalVias", + # note: the above has 0.3mm thermal vias while + # Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm_ThermalVias + # has 0.2mm which is below minimums for some fabs + { + "1": self.nsleep, + "2": self.aout1, + "3": self.gnd, # AISEN + "4": self.aout2, + "5": self.bout2, + "6": self.gnd, # BISEN + "7": self.bout1, + # '8': self.nfault, # TODO at some point + "9": self.bin1, + "10": self.bin2, + "11": self.vcp, + "12": self.vm, + "13": self.gnd, + "14": self.vint, + "15": self.ain2, + "16": self.ain1, + "17": self.gnd, # exposed pad + }, + mfr="Texas Instruments", + part="DRV8833PWP", # also compatible w/ PW package (no GND pad) + datasheet="https://www.ti.com/lit/ds/symlink/drv8833.pdf", + ) + self.assign(self.lcsc_part, "C50506") class Drv8833(BrushedMotorDriver, GeneratorBlock): - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Drv8833_Device()) - self.pwr = self.Export(self.ic.vm) - self.gnd = self.Export(self.ic.gnd, [Common]) + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Drv8833_Device()) + self.pwr = self.Export(self.ic.vm) + self.gnd = self.Export(self.ic.gnd, [Common]) - self.sleep = self.Port(DigitalSink.empty(), optional=True) # can be connected to 2-5v to avoid the resistor - self.generator_param(self.sleep.is_connected()) - self.ain1 = self.Export(self.ic.ain1, optional=True) - self.ain2 = self.Export(self.ic.ain2, optional=True) - self.bin1 = self.Export(self.ic.bin1, optional=True) - self.bin2 = self.Export(self.ic.bin2, optional=True) + self.sleep = self.Port(DigitalSink.empty(), optional=True) # can be connected to 2-5v to avoid the resistor + self.generator_param(self.sleep.is_connected()) + self.ain1 = self.Export(self.ic.ain1, optional=True) + self.ain2 = self.Export(self.ic.ain2, optional=True) + self.bin1 = self.Export(self.ic.bin1, optional=True) + self.bin2 = self.Export(self.ic.bin2, optional=True) - self.aout1 = self.Export(self.ic.aout1, optional=True) - self.aout2 = self.Export(self.ic.aout2, optional=True) - self.bout1 = self.Export(self.ic.bout1, optional=True) - self.bout2 = self.Export(self.ic.bout2, optional=True) + self.aout1 = self.Export(self.ic.aout1, optional=True) + self.aout2 = self.Export(self.ic.aout2, optional=True) + self.bout1 = self.Export(self.ic.bout1, optional=True) + self.bout2 = self.Export(self.ic.bout2, optional=True) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - # the upper tolerable range of these caps is extended to allow search flexibility when voltage derating - self.vm_cap = self.Block(DecouplingCapacitor((10*0.8, 100)*uFarad)).connected(self.gnd, self.ic.vm) - self.vint_cap = self.Block(DecouplingCapacitor((2.2*0.8, 10)*uFarad)).connected(self.gnd, self.ic.vint) - self.vcp_cap = self.Block(Capacitor(0.01*uFarad(tol=0.2), (0, 16)*Volt)) - self.connect(self.vcp_cap.pos, self.ic.vcp) - self.connect(self.vcp_cap.neg.adapt_to(VoltageSink()), self.ic.vm) + # the upper tolerable range of these caps is extended to allow search flexibility when voltage derating + self.vm_cap = self.Block(DecouplingCapacitor((10 * 0.8, 100) * uFarad)).connected(self.gnd, self.ic.vm) + self.vint_cap = self.Block(DecouplingCapacitor((2.2 * 0.8, 10) * uFarad)).connected(self.gnd, self.ic.vint) + self.vcp_cap = self.Block(Capacitor(0.01 * uFarad(tol=0.2), (0, 16) * Volt)) + self.connect(self.vcp_cap.pos, self.ic.vcp) + self.connect(self.vcp_cap.neg.adapt_to(VoltageSink()), self.ic.vm) - @override - def generate(self) -> None: - super().generate() - if self.get(self.sleep.is_connected()): - self.connect(self.sleep, self.ic.nsleep) - else: # generate default pullup, note chip is internal pulldown (disabled) - # TODO can direct connect if pwr voltage is <5.75v - self.sleep_pull = self.Block(Resistor(47*kOhm(tol=0.05))) - self.connect(self.sleep_pull.a.adapt_to(VoltageSink()), self.pwr) - self.connect(self.sleep_pull.b.adapt_to(DigitalSource()), self.ic.nsleep) + @override + def generate(self) -> None: + super().generate() + if self.get(self.sleep.is_connected()): + self.connect(self.sleep, self.ic.nsleep) + else: # generate default pullup, note chip is internal pulldown (disabled) + # TODO can direct connect if pwr voltage is <5.75v + self.sleep_pull = self.Block(Resistor(47 * kOhm(tol=0.05))) + self.connect(self.sleep_pull.a.adapt_to(VoltageSink()), self.pwr) + self.connect(self.sleep_pull.b.adapt_to(DigitalSource()), self.ic.nsleep) diff --git a/edg/parts/MotorDriver_Drv8870.py b/edg/parts/MotorDriver_Drv8870.py index ad5b11830..9d77f69e5 100644 --- a/edg/parts/MotorDriver_Drv8870.py +++ b/edg/parts/MotorDriver_Drv8870.py @@ -5,85 +5,88 @@ class Drv8870_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground()) - self.vm = self.Port(VoltageSink.from_gnd( - self.gnd, - voltage_limits=(6.5, 45)*Volt, current_draw=RangeExpr()) - ) - self.vref = self.Port(VoltageSink.from_gnd( - self.gnd, - voltage_limits=(0.3, 5)*Volt, # operational from 0-0.3v, but degraded accuracy - )) + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground()) + self.vm = self.Port(VoltageSink.from_gnd(self.gnd, voltage_limits=(6.5, 45) * Volt, current_draw=RangeExpr())) + self.vref = self.Port( + VoltageSink.from_gnd( + self.gnd, + voltage_limits=(0.3, 5) * Volt, # operational from 0-0.3v, but degraded accuracy + ) + ) - din_model = DigitalSink.from_supply( - self.gnd, self.vm, - voltage_limit_abs=(-0.3, 7)*Volt, - input_threshold_abs=(0.5, 1.5)*Volt, - pulldown_capable=True # internal 100kOhm pulldown - ) - self.in1 = self.Port(din_model) - self.in2 = self.Port(din_model) + din_model = DigitalSink.from_supply( + self.gnd, + self.vm, + voltage_limit_abs=(-0.3, 7) * Volt, + input_threshold_abs=(0.5, 1.5) * Volt, + pulldown_capable=True, # internal 100kOhm pulldown + ) + self.in1 = self.Port(din_model) + self.in2 = self.Port(din_model) - dout_model = DigitalSource.from_supply(self.gnd, self.vm, current_limits=(-3.6, 3.6)*Amp) # peak output current - self.out1 = self.Port(dout_model) - self.out2 = self.Port(dout_model) + dout_model = DigitalSource.from_supply( + self.gnd, self.vm, current_limits=(-3.6, 3.6) * Amp + ) # peak output current + self.out1 = self.Port(dout_model) + self.out2 = self.Port(dout_model) - self.isen = self.Port(VoltageSink( - current_draw=RangeExpr() - )) + self.isen = self.Port(VoltageSink(current_draw=RangeExpr())) - @override - def contents(self) -> None: - self.assign(self.isen.current_draw, - (0, self.out1.link().current_drawn.abs().upper().max( - self.out2.link().current_drawn.abs().upper()))) - self.assign(self.vm.current_draw, (10, 10000) * uAmp + # from sleep to max operating - self.isen.current_draw) + @override + def contents(self) -> None: + self.assign( + self.isen.current_draw, + (0, self.out1.link().current_drawn.abs().upper().max(self.out2.link().current_drawn.abs().upper())), + ) + self.assign(self.vm.current_draw, (10, 10000) * uAmp + self.isen.current_draw) # from sleep to max operating - self.footprint( - 'U', 'Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm', - { - '1': self.gnd, - '3': self.in1, - '2': self.in2, - '7': self.isen, - '6': self.out1, - '8': self.out2, - '5': self.vm, - '4': self.vref, - '9': self.gnd, # thermal pad - }, - mfr='Texas Instruments', part='DRV8870DDAR', # also compatible w/ PW package (no GND pad) - datasheet='https://www.ti.com/lit/ds/symlink/drv8870.pdf' - ) - self.assign(self.lcsc_part, 'C86590') - self.assign(self.actual_basic_part, False) + self.footprint( + "U", + "Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm", + { + "1": self.gnd, + "3": self.in1, + "2": self.in2, + "7": self.isen, + "6": self.out1, + "8": self.out2, + "5": self.vm, + "4": self.vref, + "9": self.gnd, # thermal pad + }, + mfr="Texas Instruments", + part="DRV8870DDAR", # also compatible w/ PW package (no GND pad) + datasheet="https://www.ti.com/lit/ds/symlink/drv8870.pdf", + ) + self.assign(self.lcsc_part, "C86590") + self.assign(self.actual_basic_part, False) class Drv8870(BrushedMotorDriver): - """Brushed DC motor driver, 6.5-45v, PWM control, internally current limited using current sense and trip point""" - def __init__(self, current_trip: RangeLike = (2, 3)*Amp) -> None: - super().__init__() - self.ic = self.Block(Drv8870_Device()) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.pwr = self.Export(self.ic.vm, [Power]) - self.vref = self.Export(self.ic.vref) + """Brushed DC motor driver, 6.5-45v, PWM control, internally current limited using current sense and trip point""" - self.in1 = self.Export(self.ic.in1) - self.in2 = self.Export(self.ic.in2) - self.out1 = self.Export(self.ic.out1) - self.out2 = self.Export(self.ic.out2) + def __init__(self, current_trip: RangeLike = (2, 3) * Amp) -> None: + super().__init__() + self.ic = self.Block(Drv8870_Device()) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.pwr = self.Export(self.ic.vm, [Power]) + self.vref = self.Export(self.ic.vref) - self.current_trip = self.ArgParameter(current_trip) + self.in1 = self.Export(self.ic.in1) + self.in2 = self.Export(self.ic.in2) + self.out1 = self.Export(self.ic.out1) + self.out2 = self.Export(self.ic.out2) - @override - def contents(self) -> None: - super().contents() - self.vm_cap0 = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.ic.vm) - # the upper tolerable range of these caps is extended to allow search flexibility when voltage derating - self.vm_cap1 = self.Block(DecouplingCapacitor((47*0.8, 100)*uFarad)).connected(self.gnd, self.ic.vm) - self.isen_res = self.Block(SeriesPowerResistor( - (1 / (10 * self.current_trip)).shrink_multiply(self.vref.link().voltage) - )).connected(self.gnd.as_voltage_source(), self.ic.isen) + self.current_trip = self.ArgParameter(current_trip) + + @override + def contents(self) -> None: + super().contents() + self.vm_cap0 = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.vm) + # the upper tolerable range of these caps is extended to allow search flexibility when voltage derating + self.vm_cap1 = self.Block(DecouplingCapacitor((47 * 0.8, 100) * uFarad)).connected(self.gnd, self.ic.vm) + self.isen_res = self.Block( + SeriesPowerResistor((1 / (10 * self.current_trip)).shrink_multiply(self.vref.link().voltage)) + ).connected(self.gnd.as_voltage_source(), self.ic.isen) diff --git a/edg/parts/MotorDriver_L293dd.py b/edg/parts/MotorDriver_L293dd.py index b5edddc0e..6293a7cf0 100644 --- a/edg/parts/MotorDriver_L293dd.py +++ b/edg/parts/MotorDriver_L293dd.py @@ -6,11 +6,13 @@ class L293dd_Device(InternalSubcircuit, FootprintBlock): def __init__(self) -> None: super().__init__() - self.vss = self.Port(VoltageSink( # logic supply voltage - voltage_limits=(4.5, 36) * Volt, current_draw=(16, 60)*mAmp) + self.vss = self.Port( + VoltageSink(voltage_limits=(4.5, 36) * Volt, current_draw=(16, 60) * mAmp) # logic supply voltage ) - self.vs = self.Port(VoltageSink( # supply voltage - voltage_limits=(self.vss.link().voltage.lower(), 36 * Volt), current_draw=RangeExpr()) + self.vs = self.Port( + VoltageSink( # supply voltage + voltage_limits=(self.vss.link().voltage.lower(), 36 * Volt), current_draw=RangeExpr() + ) ) self.gnd = self.Port(Ground()) @@ -31,13 +33,19 @@ def __init__(self) -> None: self.out3 = self.Port(dout_model, optional=True) self.out4 = self.Port(dout_model, optional=True) - self.assign(self.vs.current_draw, (2, 24) * mAmp + - (0, # calculate possible motor current, assuming A1/2 and B1/2 are coupled (and not independent) - self.out1.is_connected().then_else(self.out1.link().current_drawn.abs().upper(), 0*mAmp).max( - self.out2.is_connected().then_else(self.out2.link().current_drawn.abs().upper(), 0*mAmp)) + - self.out3.is_connected().then_else(self.out3.link().current_drawn.abs().upper(), 0*mAmp).max( - self.out4.is_connected().then_else(self.out4.link().current_drawn.abs().upper(), 0*mAmp)) - )) + self.assign( + self.vs.current_draw, + (2, 24) * mAmp + + ( + 0, # calculate possible motor current, assuming A1/2 and B1/2 are coupled (and not independent) + self.out1.is_connected() + .then_else(self.out1.link().current_drawn.abs().upper(), 0 * mAmp) + .max(self.out2.is_connected().then_else(self.out2.link().current_drawn.abs().upper(), 0 * mAmp)) + + self.out3.is_connected() + .then_else(self.out3.link().current_drawn.abs().upper(), 0 * mAmp) + .max(self.out4.is_connected().then_else(self.out4.link().current_drawn.abs().upper(), 0 * mAmp)), + ), + ) self.require(self.out1.is_connected().implies(self.in1.is_connected() & self.en1.is_connected())) self.require(self.out2.is_connected().implies(self.in2.is_connected() & self.en1.is_connected())) @@ -47,31 +55,33 @@ def __init__(self) -> None: @override def contents(self) -> None: self.footprint( - 'U', 'Package_SO:SOIC-20W_7.5x12.8mm_P1.27mm', + "U", + "Package_SO:SOIC-20W_7.5x12.8mm_P1.27mm", { - '1': self.en1, - '2': self.in1, - '3': self.out1, - '4': self.gnd, - '5': self.gnd, - '6': self.gnd, - '7': self.gnd, - '8': self.out2, - '9': self.in2, - '10': self.vs, - '11': self.en2, - '12': self.in3, - '13': self.out3, - '14': self.gnd, - '15': self.gnd, - '16': self.gnd, - '17': self.gnd, - '18': self.out4, - '19': self.in4, - '20': self.vss + "1": self.en1, + "2": self.in1, + "3": self.out1, + "4": self.gnd, + "5": self.gnd, + "6": self.gnd, + "7": self.gnd, + "8": self.out2, + "9": self.in2, + "10": self.vs, + "11": self.en2, + "12": self.in3, + "13": self.out3, + "14": self.gnd, + "15": self.gnd, + "16": self.gnd, + "17": self.gnd, + "18": self.out4, + "19": self.in4, + "20": self.vss, }, - mfr='STMicroelectronics', part='L293DD', # actual several compatible variants - datasheet='https://www.st.com/resource/en/datasheet/l293d.pdf' + mfr="STMicroelectronics", + part="L293DD", # actual several compatible variants + datasheet="https://www.st.com/resource/en/datasheet/l293d.pdf", ) diff --git a/edg/parts/Neopixel.py b/edg/parts/Neopixel.py index de5e7c0dd..64347c9e1 100644 --- a/edg/parts/Neopixel.py +++ b/edg/parts/Neopixel.py @@ -10,6 +10,7 @@ class Neopixel(Light, Block): """Abstract base class for individually-addressable, serially-connected Neopixel-type (typically RGB) LEDs and defines the pwr/gnd/din/dout interface.""" + def __init__(self) -> None: super().__init__() self.pwr = self.Port(VoltageSink.empty(), [Power]) @@ -21,86 +22,99 @@ def __init__(self) -> None: class Ws2812b(Neopixel, FootprintBlock, JlcPart): """5050-size Neopixel RGB. Specifically does NOT need extra filtering capacitors.""" + def __init__(self) -> None: super().__init__() - self.pwr.init_from(VoltageSink( - voltage_limits=(3.7, 5.3) * Volt, - current_draw=(0.6, 0.6 + 12*3) * mAmp, - )) + self.pwr.init_from( + VoltageSink( + voltage_limits=(3.7, 5.3) * Volt, + current_draw=(0.6, 0.6 + 12 * 3) * mAmp, + ) + ) self.gnd.init_from(Ground()) - self.din.init_from(DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_tolerance=(-0.3, 0.7), - input_threshold_abs=(0.7, 2.7), - # note that a more restrictive input_threshold_abs of (1.5, 2.3) was used previously - )) - self.dout.init_from(DigitalSource.from_supply( - self.gnd, self.pwr, - current_limits=0*mAmp(tol=0), - )) + self.din.init_from( + DigitalSink.from_supply( + self.gnd, + self.vdd, + voltage_limit_tolerance=(-0.3, 0.7), + input_threshold_abs=(0.7, 2.7), + # note that a more restrictive input_threshold_abs of (1.5, 2.3) was used previously + ) + ) + self.dout.init_from( + DigitalSource.from_supply( + self.gnd, + self.pwr, + current_limits=0 * mAmp(tol=0), + ) + ) @override def contents(self) -> None: self.footprint( - 'D', 'LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm', - { - '1': self.pwr, - '2': self.dout, - '3': self.gnd, - '4': self.din - }, - mfr='Worldsemi', part='WS2812B', - datasheet='https://datasheet.lcsc.com/lcsc/2106062036_Worldsemi-WS2812B-B-W_C2761795.pdf' + "D", + "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm", + {"1": self.pwr, "2": self.dout, "3": self.gnd, "4": self.din}, + mfr="Worldsemi", + part="WS2812B", + datasheet="https://datasheet.lcsc.com/lcsc/2106062036_Worldsemi-WS2812B-B-W_C2761795.pdf", ) # this is actually the WS2812E-V5 which shares similar specs to the B version, # but brighter reed and weaker blue and is available for JLC's economy assembly process # note, XL-5050RGBC-WS2812B is package compatible but the digital logic thresholds are relative to Vdd # and Vih at 0.65 Vddmax = 5.5v is 3.575, which is not compatible with the B version - self.assign(self.lcsc_part, 'C2920042') + self.assign(self.lcsc_part, "C2920042") self.assign(self.actual_basic_part, False) class Sk6812Mini_E_Device(InternalSubcircuit, JlcPart, FootprintBlock): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(3.7, 5.5) * Volt, - current_draw=(1, 1 + 12*3) * mAmp, # 1 mA static type + up to 12mA/ch - )) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(3.7, 5.5) * Volt, + current_draw=(1, 1 + 12 * 3) * mAmp, # 1 mA static type + up to 12mA/ch + ) + ) self.gnd = self.Port(Ground()) - self.din = self.Port(DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_tolerance=(-0.5, 0.5), - input_threshold_factor=(0.3, 0.7), - )) - self.dout = self.Port(DigitalSource.from_supply( - self.gnd, self.vdd, - current_limits=0*mAmp(tol=0), - ), optional=True) + self.din = self.Port( + DigitalSink.from_supply( + self.gnd, + self.vdd, + voltage_limit_tolerance=(-0.5, 0.5), + input_threshold_factor=(0.3, 0.7), + ) + ) + self.dout = self.Port( + DigitalSource.from_supply( + self.gnd, + self.vdd, + current_limits=0 * mAmp(tol=0), + ), + optional=True, + ) @override def contents(self) -> None: self.footprint( - 'D', 'edg:LED_SK6812MINI-E', - { - '1': self.vdd, - '2': self.dout, - '3': self.gnd, - '4': self.din - }, - mfr='Opsco Optoelectronics', part='SK6812MINI-E', - datasheet='https://cdn-shop.adafruit.com/product-files/4960/4960_SK6812MINI-E_REV02_EN.pdf' + "D", + "edg:LED_SK6812MINI-E", + {"1": self.vdd, "2": self.dout, "3": self.gnd, "4": self.din}, + mfr="Opsco Optoelectronics", + part="SK6812MINI-E", + datasheet="https://cdn-shop.adafruit.com/product-files/4960/4960_SK6812MINI-E_REV02_EN.pdf", ) - self.assign(self.lcsc_part, 'C5149201') + self.assign(self.lcsc_part, "C5149201") self.assign(self.actual_basic_part, False) class Sk6812Mini_E(Neopixel): """Reverse-mount (through-board) Neopixel RGB LED, commonly used for keyboard lighting.""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Sk6812Mini_E_Device()) - self.cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))) + self.cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) self.connect(self.gnd, self.device.gnd, self.cap.gnd) self.connect(self.pwr, self.device.vdd, self.cap.pwr) self.connect(self.din, self.device.din) @@ -110,45 +124,57 @@ def __init__(self) -> None: class Sk6805_Ec15_Device(InternalSubcircuit, JlcPart, FootprintBlock): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(3.7, 5.5) * Volt, - current_draw=(1, 1 + 5*3) * mAmp, # 1 mA static type + up to 5mA/ch - )) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(3.7, 5.5) * Volt, + current_draw=(1, 1 + 5 * 3) * mAmp, # 1 mA static type + up to 5mA/ch + ) + ) self.gnd = self.Port(Ground()) - self.din = self.Port(DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_tolerance=(-0.5, 0.5), - input_threshold_factor=(0.3, 0.7), - )) - self.dout = self.Port(DigitalSource.from_supply( - self.gnd, self.vdd, - current_limits=0*mAmp(tol=0), - ), optional=True) + self.din = self.Port( + DigitalSink.from_supply( + self.gnd, + self.vdd, + voltage_limit_tolerance=(-0.5, 0.5), + input_threshold_factor=(0.3, 0.7), + ) + ) + self.dout = self.Port( + DigitalSource.from_supply( + self.gnd, + self.vdd, + current_limits=0 * mAmp(tol=0), + ), + optional=True, + ) @override def contents(self) -> None: self.footprint( - 'D', 'LED_SMD:LED_SK6812_EC15_1.5x1.5mm', + "D", + "LED_SMD:LED_SK6812_EC15_1.5x1.5mm", { - '1': self.din, - '2': self.vdd, - '3': self.dout, - '4': self.gnd, + "1": self.din, + "2": self.vdd, + "3": self.dout, + "4": self.gnd, }, - mfr='Opsco Optoelectronics', part='SK6805-EC15', - datasheet='https://cdn-shop.adafruit.com/product-files/4492/Datasheet.pdf' + mfr="Opsco Optoelectronics", + part="SK6805-EC15", + datasheet="https://cdn-shop.adafruit.com/product-files/4492/Datasheet.pdf", ) - self.assign(self.lcsc_part, 'C2890035') + self.assign(self.lcsc_part, "C2890035") self.assign(self.actual_basic_part, False) class Sk6805_Ec15(Neopixel): """0606-size (1.5mm x 1.5mm) size Neopixel RGB LED. No longer allowed for JLC economic assembly.""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Sk6805_Ec15_Device()) - self.cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))) + self.cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) self.connect(self.gnd, self.device.gnd, self.cap.gnd) self.connect(self.pwr, self.device.vdd, self.cap.pwr) self.connect(self.din, self.device.din) @@ -158,44 +184,56 @@ def __init__(self) -> None: class Ws2812c_2020_Device(InternalSubcircuit, JlcPart, FootprintBlock): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(3.7, 5.3) * Volt, - current_draw=(0.001, 0.001 + 5*3) * mAmp, # 1 uA static + up to 5mA/ch - )) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(3.7, 5.3) * Volt, + current_draw=(0.001, 0.001 + 5 * 3) * mAmp, # 1 uA static + up to 5mA/ch + ) + ) self.gnd = self.Port(Ground()) - self.din = self.Port(DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_tolerance=(-0.3, 0.7), - input_threshold_abs=(0.7, 2.7), - )) - self.dout = self.Port(DigitalSource.from_supply( - self.gnd, self.vdd, - current_limits=0*mAmp(tol=0), - ), optional=True) + self.din = self.Port( + DigitalSink.from_supply( + self.gnd, + self.vdd, + voltage_limit_tolerance=(-0.3, 0.7), + input_threshold_abs=(0.7, 2.7), + ) + ) + self.dout = self.Port( + DigitalSource.from_supply( + self.gnd, + self.vdd, + current_limits=0 * mAmp(tol=0), + ), + optional=True, + ) @override def contents(self) -> None: self.footprint( - 'D', 'LED_SMD:LED_WS2812B-2020_PLCC4_2.0x2.0mm', + "D", + "LED_SMD:LED_WS2812B-2020_PLCC4_2.0x2.0mm", { - '1': self.dout, - '2': self.gnd, - '3': self.din, - '4': self.vdd, + "1": self.dout, + "2": self.gnd, + "3": self.din, + "4": self.vdd, }, - mfr='Worldsemi', part='WS2812C-2020-V1', - datasheet='https://cdn.sparkfun.com/assets/e/1/0/f/b/WS2812C-2020_V1.2_EN_19112716191654.pdf' + mfr="Worldsemi", + part="WS2812C-2020-V1", + datasheet="https://cdn.sparkfun.com/assets/e/1/0/f/b/WS2812C-2020_V1.2_EN_19112716191654.pdf", ) - self.assign(self.lcsc_part, 'C2976072') # note, -V1 version + self.assign(self.lcsc_part, "C2976072") # note, -V1 version self.assign(self.actual_basic_part, False) class Ws2812c_2020(Neopixel): """WS2812C low-power Neopixel RGB LED in 2.0x2.0. 3.3v logic-level signal compatible.""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Ws2812c_2020_Device()) - self.cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))) + self.cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) self.connect(self.gnd, self.device.gnd, self.cap.gnd) self.connect(self.pwr, self.device.vdd, self.cap.pwr) self.connect(self.din, self.device.din) @@ -205,43 +243,55 @@ def __init__(self) -> None: class Sk6812_Side_A_Device(InternalSubcircuit, FootprintBlock): def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(3.5, 5.5) * Volt, - current_draw=(1, 1 + 12*3) * mAmp, # 1 mA static type + up to 12mA/ch - )) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(3.5, 5.5) * Volt, + current_draw=(1, 1 + 12 * 3) * mAmp, # 1 mA static type + up to 12mA/ch + ) + ) self.gnd = self.Port(Ground()) - self.din = self.Port(DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_tolerance=(-0.5, 0.5), - input_threshold_factor=(0.3, 0.7), - )) - self.dout = self.Port(DigitalSource.from_supply( - self.gnd, self.vdd, - current_limits=0*mAmp(tol=0), - ), optional=True) + self.din = self.Port( + DigitalSink.from_supply( + self.gnd, + self.vdd, + voltage_limit_tolerance=(-0.5, 0.5), + input_threshold_factor=(0.3, 0.7), + ) + ) + self.dout = self.Port( + DigitalSource.from_supply( + self.gnd, + self.vdd, + current_limits=0 * mAmp(tol=0), + ), + optional=True, + ) @override def contents(self) -> None: self.footprint( - 'D', 'edg:LED_SK6812-SIDE-A', + "D", + "edg:LED_SK6812-SIDE-A", { - '1': self.din, - '2': self.vdd, - '3': self.dout, - '4': self.gnd, + "1": self.din, + "2": self.vdd, + "3": self.dout, + "4": self.gnd, }, - mfr='Normand Electronic Co Ltd', part='SK6812 SIDE-A', - datasheet='http://www.normandled.com/upload/201810/SK6812%20SIDE-A%20LED%20Datasheet.pdf' + mfr="Normand Electronic Co Ltd", + part="SK6812 SIDE-A", + datasheet="http://www.normandled.com/upload/201810/SK6812%20SIDE-A%20LED%20Datasheet.pdf", ) # potentially footprint-compatible with C2890037 class Sk6812_Side_A(Neopixel): """Side-emitting Neopixel LED, including used for keyboard edge lighting.""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Sk6812_Side_A_Device()) - self.cap = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))) + self.cap = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) self.connect(self.gnd, self.device.gnd, self.cap.gnd) self.connect(self.pwr, self.device.vdd, self.cap.pwr) self.connect(self.din, self.device.din) @@ -250,6 +300,7 @@ def __init__(self) -> None: class NeopixelArray(Light, GeneratorBlock): """An array of Neopixels""" + def __init__(self, count: IntLike): super().__init__() self.din = self.Port(DigitalSink.empty(), [Input]) @@ -278,20 +329,21 @@ def generate(self) -> None: class NeopixelArrayCircular(NeopixelArray, SvgPcbTemplateBlock): """An array of Neopixels, with a circular layout template""" + @override def _svgpcb_fn_name_adds(self) -> Optional[str]: return f"{self._svgpcb_get(self.count)}" @override def _svgpcb_template(self) -> str: - led_block = self._svgpcb_footprint_block_path_of(['led[0]']) - led_reftype, led_refnum = self._svgpcb_refdes_of(['led[0]']) + led_block = self._svgpcb_footprint_block_path_of(["led[0]"]) + led_reftype, led_refnum = self._svgpcb_refdes_of(["led[0]"]) assert led_block is not None led_footprint = self._svgpcb_footprint_of(led_block) - led_vdd_pin = self._svgpcb_pin_of(['led[0]'], ['vdd']) - led_gnd_pin = self._svgpcb_pin_of(['led[0]'], ['gnd']) - led_din_pin = self._svgpcb_pin_of(['led[0]'], ['din']) - led_dout_pin = self._svgpcb_pin_of(['led[0]'], ['dout']) + led_vdd_pin = self._svgpcb_pin_of(["led[0]"], ["vdd"]) + led_gnd_pin = self._svgpcb_pin_of(["led[0]"], ["gnd"]) + led_din_pin = self._svgpcb_pin_of(["led[0]"], ["din"]) + led_dout_pin = self._svgpcb_pin_of(["led[0]"], ["dout"]) assert all([pin is not None for pin in [led_vdd_pin, led_gnd_pin, led_din_pin, led_dout_pin]]) return f"""\ diff --git a/edg/parts/Oled_Er_Oled_022.py b/edg/parts/Oled_Er_Oled_022.py index d99bf4d88..9a5848e4e 100644 --- a/edg/parts/Oled_Er_Oled_022.py +++ b/edg/parts/Oled_Er_Oled_022.py @@ -8,56 +8,74 @@ class Er_Oled022_1_Outline(InternalSubcircuit, FootprintBlock): @override def contents(self) -> None: super().contents() - self.footprint('U', 'edg:Lcd_Er_Oled022_1_Outline', {}, - 'EastRising', 'ER-OLED022-1', - datasheet='https://www.buydisplay.com/download/manual/ER-OLED022-1_Series_Datasheet.pdf') + self.footprint( + "U", + "edg:Lcd_Er_Oled022_1_Outline", + {}, + "EastRising", + "ER-OLED022-1", + datasheet="https://www.buydisplay.com/download/manual/ER-OLED022-1_Series_Datasheet.pdf", + ) class Er_Oled022_1_Device(InternalSubcircuit, Block): """24-pin FPC connector for the ER-OLED022-1* device, based on the interfacing example https://www.buydisplay.com/download/interfacing/ER-OLED022-1_Interfacing.pdf""" + def __init__(self) -> None: super().__init__() self.conn = self.Block(Fpc050Bottom(length=24)) - vss_pin = self.conn.pins.request('3') + vss_pin = self.conn.pins.request("3") self.vss = self.Export(vss_pin.adapt_to(Ground()), [Common]) - self.connect(vss_pin, self.conn.pins.request('1'), self.conn.pins.request('24'), # NC/GND - self.conn.pins.request('2')) # VLSS, connect to VSS externally - - self.vdd = self.Export(self.conn.pins.request('5').adapt_to(VoltageSink( - voltage_limits=(2.4, 3.5)*Volt, - current_draw=(1, 300)*uAmp # sleep typ to max operating - ))) + self.connect( + vss_pin, self.conn.pins.request("1"), self.conn.pins.request("24"), self.conn.pins.request("2") # NC/GND + ) # VLSS, connect to VSS externally + + self.vdd = self.Export( + self.conn.pins.request("5").adapt_to( + VoltageSink( + voltage_limits=(2.4, 3.5) * Volt, current_draw=(1, 300) * uAmp # sleep typ to max operating + ) + ) + ) - self.vcc = self.Export(self.conn.pins.request('23').adapt_to(VoltageSink( - voltage_limits=(12.0, 13.0)*Volt, - current_draw=(0.001, 35)*mAmp # typ sleep to operating at 100% on - ))) + self.vcc = self.Export( + self.conn.pins.request("23").adapt_to( + VoltageSink( + voltage_limits=(12.0, 13.0) * Volt, + current_draw=(0.001, 35) * mAmp, # typ sleep to operating at 100% on + ) + ) + ) - self.iref = self.Export(self.conn.pins.request('21')) - self.vcomh = self.Export(self.conn.pins.request('22').adapt_to(VoltageSource( - voltage_out=self.vcc.link().voltage, - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) + self.iref = self.Export(self.conn.pins.request("21")) + self.vcomh = self.Export( + self.conn.pins.request("22").adapt_to( + VoltageSource( + voltage_out=self.vcc.link().voltage, current_limits=0 * mAmp(tol=0) # only for external capacitor + ) + ) + ) din_model = DigitalSink.from_supply( - self.vss, self.vdd, + self.vss, + self.vdd, voltage_limit_tolerance=(-0.3, 0.3), # assumed +0.3 tolerance - input_threshold_factor=(0.2, 0.8) + input_threshold_factor=(0.2, 0.8), ) - self.bs1 = self.Export(self.conn.pins.request('6').adapt_to(din_model)) - self.bs2 = self.Export(self.conn.pins.request('7').adapt_to(din_model)) + self.bs1 = self.Export(self.conn.pins.request("6").adapt_to(din_model)) + self.bs2 = self.Export(self.conn.pins.request("7").adapt_to(din_model)) - self.d0 = self.Export(self.conn.pins.request('13').adapt_to(din_model)) - self.d1 = self.Export(self.conn.pins.request('14').adapt_to(din_model)) - self.d2 = self.Export(self.conn.pins.request('15').adapt_to(din_model), optional=True) + self.d0 = self.Export(self.conn.pins.request("13").adapt_to(din_model)) + self.d1 = self.Export(self.conn.pins.request("14").adapt_to(din_model)) + self.d2 = self.Export(self.conn.pins.request("15").adapt_to(din_model), optional=True) - self.res = self.Export(self.conn.pins.request('9').adapt_to(din_model)) - self.cs = self.Export(self.conn.pins.request('8').adapt_to(din_model)) - self.dc = self.Export(self.conn.pins.request('10').adapt_to(din_model)) + self.res = self.Export(self.conn.pins.request("9").adapt_to(din_model)) + self.cs = self.Export(self.conn.pins.request("8").adapt_to(din_model)) + self.dc = self.Export(self.conn.pins.request("10").adapt_to(din_model)) for i in [12, 11] + list(range(16, 21)): # RW, ER, DB3~DB7 self.connect(vss_pin, self.conn.pins.request(str(i))) @@ -65,6 +83,7 @@ def __init__(self) -> None: class Er_Oled022_1(Oled, Resettable, GeneratorBlock): """SSD1305-based 2.2" 128x32 monochrome OLED in SPI or I2C mode.""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Er_Oled022_1_Device()) @@ -85,20 +104,24 @@ def contents(self) -> None: self.lcd = self.Block(Er_Oled022_1_Outline()) # for device outline - self.iref_res = self.Block(Resistor(resistance=910*kOhm(tol=0.05))) # TODO dynamic sizing + self.iref_res = self.Block(Resistor(resistance=910 * kOhm(tol=0.05))) # TODO dynamic sizing self.connect(self.iref_res.a, self.device.iref) self.connect(self.iref_res.b.adapt_to(Ground()), self.gnd) - self.vcomh_cap = self.Block(DecouplingCapacitor(4.7*uFarad(tol=0.2))).connected(self.gnd, self.device.vcomh) + self.vcomh_cap = self.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(self.gnd, self.device.vcomh) - self.vdd_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vdd) - self.vdd_cap2 = self.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vdd) + self.vdd_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd + ) + self.vdd_cap2 = self.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd + ) - self.vcc_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vcc) - self.vcc_cap2 = self.Block(DecouplingCapacitor(capacitance=10*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vcc) + self.vcc_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcc + ) + self.vcc_cap2 = self.Block(DecouplingCapacitor(capacitance=10 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcc + ) @override def generate(self) -> None: @@ -107,7 +130,9 @@ def generate(self) -> None: gnd_digital = self.gnd.as_digital_source() if self.get(self.i2c.is_connected()): - pwr_digital = self.pwr.as_digital_source() # workaround for issue #259: if this is never used it creates a broken empty adapter + pwr_digital = ( + self.pwr.as_digital_source() + ) # workaround for issue #259: if this is never used it creates a broken empty adapter self.connect(self.device.bs1, pwr_digital) self.connect(self.device.bs2, gnd_digital) @@ -116,7 +141,7 @@ def generate(self) -> None: self.connect(self.device.dc, gnd_digital) # addr, TODO support I2C addr self.connect(self.device.cs, gnd_digital) self.require(~self.spi.is_connected() & ~self.cs.is_connected() & ~self.dc.is_connected()) - self.assign(self.i2c.addresses, [0x3c]) + self.assign(self.i2c.addresses, [0x3C]) elif self.get(self.spi.is_connected()): self.connect(self.device.bs1, gnd_digital) self.connect(self.device.bs2, gnd_digital) diff --git a/edg/parts/Oled_Er_Oled_028.py b/edg/parts/Oled_Er_Oled_028.py index 0e73c880c..d341bd1e8 100644 --- a/edg/parts/Oled_Er_Oled_028.py +++ b/edg/parts/Oled_Er_Oled_028.py @@ -8,60 +8,88 @@ class Er_Oled028_1_Outline(InternalSubcircuit, FootprintBlock): @override def contents(self) -> None: super().contents() - self.footprint('U', 'edg:Lcd_Er_Oled028_1_Outline', {}, - 'EastRising', 'ER-OLED028-1', - datasheet='https://www.buydisplay.com/download/manual/ER-OLED028-1_Series_Datasheet.pdf') + self.footprint( + "U", + "edg:Lcd_Er_Oled028_1_Outline", + {}, + "EastRising", + "ER-OLED028-1", + datasheet="https://www.buydisplay.com/download/manual/ER-OLED028-1_Series_Datasheet.pdf", + ) class Er_Oled028_1_Device(InternalSubcircuit, Block): """30-pin FPC connector for the ER-OLED028-1* device, based on the interfacing example https://www.buydisplay.com/download/interfacing/ER-OLED028-1_Interfacing.pdf""" + def __init__(self) -> None: super().__init__() self.conn = self.Block(Fpc050Bottom(length=30)) - vss_pin = self.conn.pins.request('2') + vss_pin = self.conn.pins.request("2") self.vss = self.Export(vss_pin.adapt_to(Ground()), [Common]) - self.connect(vss_pin, self.conn.pins.request('1'), self.conn.pins.request('30'), # NC/GND - self.conn.pins.request('5'), self.conn.pins.request('28')) # VLSS, connect to VSS externally - - vcc3_pin = self.conn.pins.request('3') - self.connect(vcc3_pin, self.conn.pins.request('29')) - self.vcc = self.Export(vcc3_pin.adapt_to(VoltageSink( - voltage_limits=(11.5, 12.5)*Volt, - current_draw=(15.6, 60)*mAmp # typ 30% display to abs max - ))) - - self.vcomh = self.Export(self.conn.pins.request('4').adapt_to(VoltageSource( - voltage_out=self.vcc.link().voltage, - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - self.vdd = self.Export(self.conn.pins.request('25').adapt_to(VoltageSource( - voltage_out=(2.4, 2.6)*Volt, - current_limits=0*mAmp(tol=0) # only for external capacitor - ))) - self.vci = self.Export(self.conn.pins.request('26').adapt_to(VoltageSink( - voltage_limits=(2.4, 3.5)*Volt, - current_draw=(20, 300)*uAmp # typ sleep to max operating - ))) - self.vddio = self.Export(self.conn.pins.request('24').adapt_to(VoltageSink( - voltage_limits=(1.65*Volt, self.vci.link().voltage.upper()), - current_draw=(2, 10)*uAmp # typ sleep to max sleep, no operating draw given - ))) + self.connect( + vss_pin, + self.conn.pins.request("1"), + self.conn.pins.request("30"), # NC/GND + self.conn.pins.request("5"), + self.conn.pins.request("28"), + ) # VLSS, connect to VSS externally + + vcc3_pin = self.conn.pins.request("3") + self.connect(vcc3_pin, self.conn.pins.request("29")) + self.vcc = self.Export( + vcc3_pin.adapt_to( + VoltageSink( + voltage_limits=(11.5, 12.5) * Volt, current_draw=(15.6, 60) * mAmp # typ 30% display to abs max + ) + ) + ) + + self.vcomh = self.Export( + self.conn.pins.request("4").adapt_to( + VoltageSource( + voltage_out=self.vcc.link().voltage, current_limits=0 * mAmp(tol=0) # only for external capacitor + ) + ) + ) + self.vdd = self.Export( + self.conn.pins.request("25").adapt_to( + VoltageSource( + voltage_out=(2.4, 2.6) * Volt, current_limits=0 * mAmp(tol=0) # only for external capacitor + ) + ) + ) + self.vci = self.Export( + self.conn.pins.request("26").adapt_to( + VoltageSink( + voltage_limits=(2.4, 3.5) * Volt, current_draw=(20, 300) * uAmp # typ sleep to max operating + ) + ) + ) + self.vddio = self.Export( + self.conn.pins.request("24").adapt_to( + VoltageSink( + voltage_limits=(1.65 * Volt, self.vci.link().voltage.upper()), + current_draw=(2, 10) * uAmp, # typ sleep to max sleep, no operating draw given + ) + ) + ) din_model = DigitalSink.from_supply( - self.vss, self.vddio, + self.vss, + self.vddio, voltage_limit_tolerance=(-0.5, 0.3), # assumed +0.3 tolerance - input_threshold_factor=(0.2, 0.8) + input_threshold_factor=(0.2, 0.8), ) - self.bs0 = self.Export(self.conn.pins.request('16').adapt_to(din_model)) # 3-wire (1) / 4-wire (0) serial - self.connect(self.conn.pins.request('17').adapt_to(Ground()), self.vss) # BS1, 0 for any serial + self.bs0 = self.Export(self.conn.pins.request("16").adapt_to(din_model)) # 3-wire (1) / 4-wire (0) serial + self.connect(self.conn.pins.request("17").adapt_to(Ground()), self.vss) # BS1, 0 for any serial self.spi = self.Port(SpiPeripheral.empty()) - self.connect(self.spi.sck, self.conn.pins.request('13').adapt_to(din_model)) # DB0 - self.connect(self.spi.mosi, self.conn.pins.request('12').adapt_to(din_model)) # DB1 + self.connect(self.spi.sck, self.conn.pins.request("13").adapt_to(din_model)) # DB0 + self.connect(self.spi.mosi, self.conn.pins.request("12").adapt_to(din_model)) # DB1 self.miso_nc = self.Block(DigitalBidirNotConnected()) self.connect(self.spi.miso, self.miso_nc.port) @@ -69,16 +97,17 @@ def __init__(self) -> None: for i in list(range(6, 11)) + [15, 14]: # DB7~DB3, RW, ER self.connect(vss_pin, self.conn.pins.request(str(i))) - self.dc = self.Export(self.conn.pins.request('18').adapt_to(din_model)) # ground if unused - self.cs = self.Export(self.conn.pins.request('19').adapt_to(din_model)) - self.res = self.Export(self.conn.pins.request('20').adapt_to(din_model)) + self.dc = self.Export(self.conn.pins.request("18").adapt_to(din_model)) # ground if unused + self.cs = self.Export(self.conn.pins.request("19").adapt_to(din_model)) + self.res = self.Export(self.conn.pins.request("20").adapt_to(din_model)) # pin 21 FR disconnected - self.iref = self.Export(self.conn.pins.request('22')) - self.vsl = self.Export(self.conn.pins.request('27')) + self.iref = self.Export(self.conn.pins.request("22")) + self.vsl = self.Export(self.conn.pins.request("27")) class Er_Oled028_1(Oled, Resettable, GeneratorBlock): """SSD1322-based 2.8" 256x64 monochrome OLED.""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Er_Oled028_1_Device()) @@ -100,26 +129,30 @@ def contents(self) -> None: self.lcd = self.Block(Er_Oled028_1_Outline()) # for device outline - self.iref_res = self.Block(Resistor(resistance=680*kOhm(tol=0.05))) # TODO dynamic sizing + self.iref_res = self.Block(Resistor(resistance=680 * kOhm(tol=0.05))) # TODO dynamic sizing self.connect(self.iref_res.a, self.device.iref) self.connect(self.iref_res.b.adapt_to(Ground()), self.gnd) - self.vcomh_cap = self.Block(DecouplingCapacitor(4.7*uFarad(tol=0.2))).connected(self.gnd, self.device.vcomh) - - self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vdd) - self.vci_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vci) - self.vci_cap2 = self.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vci) - - self.vcc_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vcc) - self.vcc_cap2 = self.Block(DecouplingCapacitor(capacitance=10*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vcc) - - self.vsl_res = self.Block(Resistor(resistance=50*kOhm(tol=0.05))) - diode_model = Diode(reverse_voltage=(0, 0)*Volt, current=(0, 0)*Amp, - voltage_drop=(0, 1.2)*Volt) + self.vcomh_cap = self.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(self.gnd, self.device.vcomh) + + self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd + ) + self.vci_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vci + ) + self.vci_cap2 = self.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vci + ) + + self.vcc_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcc + ) + self.vcc_cap2 = self.Block(DecouplingCapacitor(capacitance=10 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcc + ) + + self.vsl_res = self.Block(Resistor(resistance=50 * kOhm(tol=0.05))) + diode_model = Diode(reverse_voltage=(0, 0) * Volt, current=(0, 0) * Amp, voltage_drop=(0, 1.2) * Volt) self.vsl_d1 = self.Block(diode_model) self.vsl_d2 = self.Block(diode_model) self.connect(self.device.vsl, self.vsl_res.a) diff --git a/edg/parts/Oled_Er_Oled_091_3.py b/edg/parts/Oled_Er_Oled_091_3.py index 6705cf55e..3789df9e4 100644 --- a/edg/parts/Oled_Er_Oled_091_3.py +++ b/edg/parts/Oled_Er_Oled_091_3.py @@ -6,71 +6,96 @@ class Er_Oled_091_3_Outline(InternalSubcircuit, FootprintBlock): """Footprint for OLED panel outline""" + @override def contents(self) -> None: super().contents() - self.footprint('U', 'edg:Lcd_Er_Oled0.91_3_Outline', {}, - 'EastRising', 'ER-OLED-0.91-3', - datasheet='https://www.buydisplay.com/download/manual/ER-OLED0.91-3_Series_Datasheet.pdf') + self.footprint( + "U", + "edg:Lcd_Er_Oled0.91_3_Outline", + {}, + "EastRising", + "ER-OLED-0.91-3", + datasheet="https://www.buydisplay.com/download/manual/ER-OLED0.91-3_Series_Datasheet.pdf", + ) class Er_Oled_091_3_Device(InternalSubcircuit, Nonstrict3v3Compatible, Block): """15-pin FPC connector for the ER-OLED-0.91-3* device, configured to run off internal DC/DC""" + def __init__(self) -> None: super().__init__() self.conn = self.Block(Fpc050Bottom(length=15)) - self.vcc = self.Export(self.conn.pins.request('15').adapt_to(VoltageSource( - voltage_out=(6.4, 9)*Volt, - current_limits=0*mAmp(tol=0) # external draw not allowed, probably does 10-16mA - ))) - self.vcomh = self.Export(self.conn.pins.request('14').adapt_to(VoltageSource( - voltage_out=self.vcc.voltage_out, # can program Vcomh to be fractions of Vcc - current_limits=0*mAmp(tol=0) # external draw not allowed - ))) - self.vdd = self.Export(self.conn.pins.request('7').adapt_to(VoltageSink( - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (1.65, 3.6)*Volt, # abs max is 4v - (1.65, 3.3)*Volt), - current_draw=(1, 300)*uAmp - ))) - self.vbat = self.Export(self.conn.pins.request('5').adapt_to(VoltageSink( - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (3.1, 4.2)*Volt, # technically out of spec, works in practice near 3.3v - (3.3, 4.2)*Volt), # 3.3 lower from SSD1306 datasheet v1.6, panel datasheet more restrictive - current_draw=(23, 29)*mAmp - ))) - self.vss = self.Export(self.conn.pins.request('6').adapt_to(Ground()), [Common]) + self.vcc = self.Export( + self.conn.pins.request("15").adapt_to( + VoltageSource( + voltage_out=(6.4, 9) * Volt, + current_limits=0 * mAmp(tol=0), # external draw not allowed, probably does 10-16mA + ) + ) + ) + self.vcomh = self.Export( + self.conn.pins.request("14").adapt_to( + VoltageSource( + voltage_out=self.vcc.voltage_out, # can program Vcomh to be fractions of Vcc + current_limits=0 * mAmp(tol=0), # external draw not allowed + ) + ) + ) + self.vdd = self.Export( + self.conn.pins.request("7").adapt_to( + VoltageSink( + voltage_limits=self.nonstrict_3v3_compatible.then_else( + (1.65, 3.6) * Volt, (1.65, 3.3) * Volt # abs max is 4v + ), + current_draw=(1, 300) * uAmp, + ) + ) + ) + self.vbat = self.Export( + self.conn.pins.request("5").adapt_to( + VoltageSink( + voltage_limits=self.nonstrict_3v3_compatible.then_else( + (3.1, 4.2) * Volt, (3.3, 4.2) * Volt # technically out of spec, works in practice near 3.3v + ), # 3.3 lower from SSD1306 datasheet v1.6, panel datasheet more restrictive + current_draw=(23, 29) * mAmp, + ) + ) + ) + self.vss = self.Export(self.conn.pins.request("6").adapt_to(Ground()), [Common]) din_model = DigitalSink.from_supply( - self.vss, self.vdd, + self.vss, + self.vdd, voltage_limit_tolerance=(-0.3, 0.3), # SSD1306 datasheet, Table 11-1 - input_threshold_factor=(0.2, 0.8) + input_threshold_factor=(0.2, 0.8), ) self.spi = self.Port(SpiPeripheral.empty()) - self.connect(self.spi.sck, self.conn.pins.request('11').adapt_to(din_model)) - self.connect(self.spi.mosi, self.conn.pins.request('12').adapt_to(din_model)) + self.connect(self.spi.sck, self.conn.pins.request("11").adapt_to(din_model)) + self.connect(self.spi.mosi, self.conn.pins.request("12").adapt_to(din_model)) self.miso_nc = self.Block(DigitalBidirNotConnected()) self.connect(self.spi.miso, self.miso_nc.port) - self.dc = self.Export(self.conn.pins.request('10').adapt_to(din_model)) - self.res = self.Export(self.conn.pins.request('9').adapt_to(din_model)) - self.cs = self.Export(self.conn.pins.request('8').adapt_to(din_model)) + self.dc = self.Export(self.conn.pins.request("10").adapt_to(din_model)) + self.res = self.Export(self.conn.pins.request("9").adapt_to(din_model)) + self.cs = self.Export(self.conn.pins.request("8").adapt_to(din_model)) - self.iref = self.Export(self.conn.pins.request('13')) - self.c2p = self.Export(self.conn.pins.request('1')) - self.c2n = self.Export(self.conn.pins.request('2')) - self.c1p = self.Export(self.conn.pins.request('3')) - self.c1n = self.Export(self.conn.pins.request('4')) + self.iref = self.Export(self.conn.pins.request("13")) + self.c2p = self.Export(self.conn.pins.request("1")) + self.c2n = self.Export(self.conn.pins.request("2")) + self.c1p = self.Export(self.conn.pins.request("3")) + self.c1n = self.Export(self.conn.pins.request("4")) class Er_Oled_091_3(Oled, Resettable, Block): """SSD1306-based 0.91" 128x32 monochrome OLED. TODO (maybe?) add the power gating circuit in the reference schematic""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Er_Oled_091_3_Device()) @@ -89,21 +114,25 @@ def contents(self) -> None: self.lcd = self.Block(Er_Oled_091_3_Outline()) # for device outline - self.c1_cap = self.Block(Capacitor(0.1*uFarad(tol=0.2), (0, 6.3)*Volt)) + self.c1_cap = self.Block(Capacitor(0.1 * uFarad(tol=0.2), (0, 6.3) * Volt)) self.connect(self.c1_cap.pos, self.device.c1p) self.connect(self.c1_cap.neg, self.device.c1n) - self.c2_cap = self.Block(Capacitor(0.1*uFarad(tol=0.2), (0, 6.3)*Volt)) + self.c2_cap = self.Block(Capacitor(0.1 * uFarad(tol=0.2), (0, 6.3) * Volt)) self.connect(self.c2_cap.pos, self.device.c2p) self.connect(self.c2_cap.neg, self.device.c2n) - self.iref_res = self.Block(Resistor(resistance=560*kOhm(tol=0.05))) # TODO dynamic sizing + self.iref_res = self.Block(Resistor(resistance=560 * kOhm(tol=0.05))) # TODO dynamic sizing self.connect(self.iref_res.a, self.device.iref) self.connect(self.iref_res.b.adapt_to(Ground()), self.gnd) - self.vcomh_cap = self.Block(DecouplingCapacitor((2.2*.8, 10)*uFarad)).connected(self.gnd, self.device.vcomh) + self.vcomh_cap = self.Block(DecouplingCapacitor((2.2 * 0.8, 10) * uFarad)).connected( + self.gnd, self.device.vcomh + ) - self.vdd_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.vdd_cap2 = self.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + self.vdd_cap2 = self.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) - self.vcc_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vcc) - self.vcc_cap2 = self.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vcc) + self.vcc_cap1 = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcc + ) + self.vcc_cap2 = self.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcc + ) diff --git a/edg/parts/Oled_Er_Oled_096_1_1.py b/edg/parts/Oled_Er_Oled_096_1_1.py index 0da14e85f..d2e13ecc4 100644 --- a/edg/parts/Oled_Er_Oled_096_1_1.py +++ b/edg/parts/Oled_Er_Oled_096_1_1.py @@ -6,72 +6,94 @@ class Er_Oled_096_1_1_Outline(InternalSubcircuit, FootprintBlock): """Footprint for OLED panel outline""" + @override def contents(self) -> None: super().contents() - self.footprint('U', 'edg:Lcd_Er_Oled0.96_1.1_Outline', {}, - 'EastRising', 'ER-OLED-0.96-1.1', - datasheet='https://www.buydisplay.com/download/manual/ER-OLED0.96-1_Series_Datasheet.pdf') + self.footprint( + "U", + "edg:Lcd_Er_Oled0.96_1.1_Outline", + {}, + "EastRising", + "ER-OLED-0.96-1.1", + datasheet="https://www.buydisplay.com/download/manual/ER-OLED0.96-1_Series_Datasheet.pdf", + ) class Er_Oled_096_1_1_Device(InternalSubcircuit, Nonstrict3v3Compatible, Block): """30-pin FPC connector for the ER-OLED-0.96-1.1* device, configured to run off internal DC/DC""" + def __init__(self) -> None: super().__init__() self.conn = self.Block(Fpc050Bottom(length=30)) - vss_pin = self.conn.pins.request('8') + vss_pin = self.conn.pins.request("8") self.vss = self.Export(vss_pin.adapt_to(Ground()), [Common]) - self.connect(vss_pin, self.conn.pins.request('1'), self.conn.pins.request('30'), # NC/GND - self.conn.pins.request('29')) # VLSS - - self.vdd = self.Export(self.conn.pins.request('9').adapt_to(VoltageSink( - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (1.65, 3.6)*Volt, # abs max is 4v - (1.65, 3.3)*Volt), - current_draw=(1, 300)*uAmp # sleep to operating - ))) - - self.vcc = self.Export(self.conn.pins.request('28').adapt_to(VoltageSource( - voltage_out=(7, 7.5)*Volt, - current_limits=0*mAmp(tol=0) - ))) - - self.iref = self.Export(self.conn.pins.request('26')) - self.vcomh = self.Export(self.conn.pins.request('27').adapt_to(VoltageSource( - voltage_out=self.vcc.voltage_out, # can program Vcomh to be fractions of Vcc - current_limits=0*mAmp(tol=0) # external draw not allowed - ))) - - self.vbat = self.Export(self.conn.pins.request('6').adapt_to(VoltageSink( - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (3.1, 4.2)*Volt, # technically out of spec, works in practice near 3.3v - (3.3, 4.2)*Volt), # 3.3 lower from SSD1306 datasheet v1.6, panel datasheet more restrictive - current_draw=(18.8, 32.0)*mAmp # typ @ 50% on to max @ 100% on - ))) - self.c1p = self.Export(self.conn.pins.request('4')) - self.c1n = self.Export(self.conn.pins.request('5')) - self.c2p = self.Export(self.conn.pins.request('2')) - self.c2n = self.Export(self.conn.pins.request('3')) + self.connect( + vss_pin, self.conn.pins.request("1"), self.conn.pins.request("30"), self.conn.pins.request("29") # NC/GND + ) # VLSS + + self.vdd = self.Export( + self.conn.pins.request("9").adapt_to( + VoltageSink( + voltage_limits=self.nonstrict_3v3_compatible.then_else( + (1.65, 3.6) * Volt, (1.65, 3.3) * Volt # abs max is 4v + ), + current_draw=(1, 300) * uAmp, # sleep to operating + ) + ) + ) + + self.vcc = self.Export( + self.conn.pins.request("28").adapt_to( + VoltageSource(voltage_out=(7, 7.5) * Volt, current_limits=0 * mAmp(tol=0)) + ) + ) + + self.iref = self.Export(self.conn.pins.request("26")) + self.vcomh = self.Export( + self.conn.pins.request("27").adapt_to( + VoltageSource( + voltage_out=self.vcc.voltage_out, # can program Vcomh to be fractions of Vcc + current_limits=0 * mAmp(tol=0), # external draw not allowed + ) + ) + ) + + self.vbat = self.Export( + self.conn.pins.request("6").adapt_to( + VoltageSink( + voltage_limits=self.nonstrict_3v3_compatible.then_else( + (3.1, 4.2) * Volt, (3.3, 4.2) * Volt # technically out of spec, works in practice near 3.3v + ), # 3.3 lower from SSD1306 datasheet v1.6, panel datasheet more restrictive + current_draw=(18.8, 32.0) * mAmp, # typ @ 50% on to max @ 100% on + ) + ) + ) + self.c1p = self.Export(self.conn.pins.request("4")) + self.c1n = self.Export(self.conn.pins.request("5")) + self.c2p = self.Export(self.conn.pins.request("2")) + self.c2n = self.Export(self.conn.pins.request("3")) din_model = DigitalSink.from_supply( - self.vss, self.vdd, + self.vss, + self.vdd, voltage_limit_tolerance=(-0.3, 0.3), # SSD1306 datasheet, Table 11-1 - input_threshold_factor=(0.2, 0.8) + input_threshold_factor=(0.2, 0.8), ) - self.bs0 = self.Export(self.conn.pins.request('10').adapt_to(din_model)) - self.bs1 = self.Export(self.conn.pins.request('11').adapt_to(din_model)) - self.connect(self.conn.pins.request('12').adapt_to(Ground()), self.vss) # BS2, 0 for any serial + self.bs0 = self.Export(self.conn.pins.request("10").adapt_to(din_model)) + self.bs1 = self.Export(self.conn.pins.request("11").adapt_to(din_model)) + self.connect(self.conn.pins.request("12").adapt_to(Ground()), self.vss) # BS2, 0 for any serial - self.res = self.Export(self.conn.pins.request('14').adapt_to(din_model)) - self.cs = self.Export(self.conn.pins.request('13').adapt_to(din_model)) - self.dc = self.Export(self.conn.pins.request('15').adapt_to(din_model)) + self.res = self.Export(self.conn.pins.request("14").adapt_to(din_model)) + self.cs = self.Export(self.conn.pins.request("13").adapt_to(din_model)) + self.dc = self.Export(self.conn.pins.request("15").adapt_to(din_model)) - self.d0 = self.Export(self.conn.pins.request('18').adapt_to(din_model)) - self.d1 = self.Export(self.conn.pins.request('19').adapt_to(din_model)) - self.d2 = self.Export(self.conn.pins.request('20').adapt_to(din_model), optional=True) + self.d0 = self.Export(self.conn.pins.request("18").adapt_to(din_model)) + self.d1 = self.Export(self.conn.pins.request("19").adapt_to(din_model)) + self.d2 = self.Export(self.conn.pins.request("20").adapt_to(din_model), optional=True) for i in [17, 16] + list(range(21, 26)): # RW, ER, DB3~DB7 self.connect(vss_pin, self.conn.pins.request(str(i))) @@ -79,6 +101,7 @@ def __init__(self) -> None: class Er_Oled_096_1_1(Oled, Resettable, GeneratorBlock): """SSD1306-based 0.96" 128x64 monochrome OLED, in either I2C or SPI mode.""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Er_Oled_096_1_1_Device()) @@ -99,23 +122,26 @@ def contents(self) -> None: self.lcd = self.Block(Er_Oled_096_1_1_Outline()) # for device outline - self.c1_cap = self.Block(Capacitor(1*uFarad(tol=0.2), (0, 6.3)*Volt)) + self.c1_cap = self.Block(Capacitor(1 * uFarad(tol=0.2), (0, 6.3) * Volt)) self.connect(self.c1_cap.pos, self.device.c1p) self.connect(self.c1_cap.neg, self.device.c1n) - self.c2_cap = self.Block(Capacitor(1*uFarad(tol=0.2), (0, 6.3)*Volt)) + self.c2_cap = self.Block(Capacitor(1 * uFarad(tol=0.2), (0, 6.3) * Volt)) self.connect(self.c2_cap.pos, self.device.c2p) self.connect(self.c2_cap.neg, self.device.c2n) - self.iref_res = self.Block(Resistor(resistance=390*kOhm(tol=0.05))) # TODO dynamic sizing + self.iref_res = self.Block(Resistor(resistance=390 * kOhm(tol=0.05))) # TODO dynamic sizing self.connect(self.iref_res.a, self.device.iref) self.connect(self.iref_res.b.adapt_to(Ground()), self.gnd) - self.vcomh_cap = self.Block(DecouplingCapacitor(4.7*uFarad(tol=0.2))).connected(self.gnd, self.device.vcomh) + self.vcomh_cap = self.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(self.gnd, self.device.vcomh) - self.vdd_cap1 = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vdd) - self.vbat_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vbat) - self.vcc_cap = self.Block(DecouplingCapacitor(capacitance=(2.2*0.8, 10)*uFarad))\ - .connected(self.gnd, self.device.vcc) + self.vdd_cap1 = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd + ) + self.vbat_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vbat + ) + self.vcc_cap = self.Block(DecouplingCapacitor(capacitance=(2.2 * 0.8, 10) * uFarad)).connected( + self.gnd, self.device.vcc + ) @override def generate(self) -> None: @@ -124,7 +150,9 @@ def generate(self) -> None: gnd_digital = self.gnd.as_digital_source() if self.get(self.i2c.is_connected()): - pwr_digital = self.pwr.as_digital_source() # workaround for issue #259: if this is never used it creates a broken empty adapter + pwr_digital = ( + self.pwr.as_digital_source() + ) # workaround for issue #259: if this is never used it creates a broken empty adapter self.connect(self.device.bs0, gnd_digital) self.connect(self.device.bs1, pwr_digital) @@ -133,7 +161,7 @@ def generate(self) -> None: self.connect(self.device.dc, gnd_digital) # addr, TODO support I2C addr self.connect(self.device.cs, gnd_digital) self.require(~self.spi.is_connected() & ~self.cs.is_connected() & ~self.dc.is_connected()) - self.assign(self.i2c.addresses, [0x3c]) + self.assign(self.i2c.addresses, [0x3C]) elif self.get(self.spi.is_connected()): self.connect(self.device.bs1, gnd_digital) self.connect(self.spi.sck, self.device.d0) diff --git a/edg/parts/Oled_Er_Oled_096_1c.py b/edg/parts/Oled_Er_Oled_096_1c.py index 0ae925757..02f5cd0b3 100644 --- a/edg/parts/Oled_Er_Oled_096_1c.py +++ b/edg/parts/Oled_Er_Oled_096_1c.py @@ -6,72 +6,97 @@ class Er_Oled_096_1c_Outline(InternalSubcircuit, FootprintBlock): """Footprint for OLED panel outline""" + @override def contents(self) -> None: super().contents() - self.footprint('U', 'edg:Lcd_Er_Oled0.96_1c_Outline', {}, - 'EastRising', 'ER-OLED0.96-1C', - datasheet='https://www.buydisplay.com/download/manual/ER-OLED0.96-1C_Datasheet.pdf') + self.footprint( + "U", + "edg:Lcd_Er_Oled0.96_1c_Outline", + {}, + "EastRising", + "ER-OLED0.96-1C", + datasheet="https://www.buydisplay.com/download/manual/ER-OLED0.96-1C_Datasheet.pdf", + ) class Er_Oled_096_1c_Device(InternalSubcircuit, Block): """31-pin FPC connector for the ER-OLED0.96-1C device""" + def __init__(self) -> None: super().__init__() self.conn = self.Block(Fpc030Bottom(length=31)) - vss_pin = self.conn.pins.request('7') + vss_pin = self.conn.pins.request("7") self.vss = self.Export(vss_pin.adapt_to(Ground()), [Common]) - self.connect(vss_pin, self.conn.pins.request('1'), self.conn.pins.request('31'), # VLSS - self.conn.pins.request('26')) # VLL - - self.vdd = self.Export(self.conn.pins.request('8').adapt_to(VoltageSink( - voltage_limits=(1.65, 3.5)*Volt, # abs max is 4v - current_draw=(10, 800)*uAmp # sleep mode current to supply current from SSD1357 datasheet - ))) - self.connect(self.vdd, self.conn.pins.request('17').adapt_to(VoltageSink())) - - vcc_pin5 = self.conn.pins.request('5') - self.vcc = self.Export(vcc_pin5.adapt_to(VoltageSink( - voltage_limits=(14.5, 15.5)*Volt, # abs max is 8-19v - current_draw=(0.010, 46)*mAmp # sleep mode current to normal mode with all pixels on - ))) - self.connect(vcc_pin5, self.conn.pins.request('27')) # connection upstream of adapter - - self.iref = self.Export(self.conn.pins.request('6')) - self.vcomh = self.Export(self.conn.pins.request('3').adapt_to(VoltageSource( - voltage_out=self.vcc.link().voltage * 0.86, # selectable up to 0.86 Vcc by command BEh - current_limits=0*mAmp(tol=0) # external draw not allowed - ))) - self.connect(self.vcomh, self.conn.pins.request('29').adapt_to(VoltageSink())) - - self.vsl = self.Export(self.conn.pins.request('2')) - self.connect(self.vsl, self.conn.pins.request('30')) - - self.vp = self.Export(self.conn.pins.request('4').adapt_to(VoltageSource( - voltage_out=self.vcc.link().voltage * 0.5133, # selectable up to 0.5133 Vcc by command BBh - current_limits=0*mAmp(tol=0) # external draw not allowed - ))) - self.connect(self.vp, self.conn.pins.request('28').adapt_to(VoltageSink())) + self.connect( + vss_pin, self.conn.pins.request("1"), self.conn.pins.request("31"), self.conn.pins.request("26") # VLSS + ) # VLL + + self.vdd = self.Export( + self.conn.pins.request("8").adapt_to( + VoltageSink( + voltage_limits=(1.65, 3.5) * Volt, # abs max is 4v + current_draw=(10, 800) * uAmp, # sleep mode current to supply current from SSD1357 datasheet + ) + ) + ) + self.connect(self.vdd, self.conn.pins.request("17").adapt_to(VoltageSink())) + + vcc_pin5 = self.conn.pins.request("5") + self.vcc = self.Export( + vcc_pin5.adapt_to( + VoltageSink( + voltage_limits=(14.5, 15.5) * Volt, # abs max is 8-19v + current_draw=(0.010, 46) * mAmp, # sleep mode current to normal mode with all pixels on + ) + ) + ) + self.connect(vcc_pin5, self.conn.pins.request("27")) # connection upstream of adapter + + self.iref = self.Export(self.conn.pins.request("6")) + self.vcomh = self.Export( + self.conn.pins.request("3").adapt_to( + VoltageSource( + voltage_out=self.vcc.link().voltage * 0.86, # selectable up to 0.86 Vcc by command BEh + current_limits=0 * mAmp(tol=0), # external draw not allowed + ) + ) + ) + self.connect(self.vcomh, self.conn.pins.request("29").adapt_to(VoltageSink())) + + self.vsl = self.Export(self.conn.pins.request("2")) + self.connect(self.vsl, self.conn.pins.request("30")) + + self.vp = self.Export( + self.conn.pins.request("4").adapt_to( + VoltageSource( + voltage_out=self.vcc.link().voltage * 0.5133, # selectable up to 0.5133 Vcc by command BBh + current_limits=0 * mAmp(tol=0), # external draw not allowed + ) + ) + ) + self.connect(self.vp, self.conn.pins.request("28").adapt_to(VoltageSink())) din_model = DigitalSink.from_supply( - self.vss, self.vdd, + self.vss, + self.vdd, voltage_limit_tolerance=(-0.3, 0.3), # SSD1357 datasheet, Table 7-1 and 8-1 - input_threshold_factor=(0.2, 0.8) + input_threshold_factor=(0.2, 0.8), ) - self.bs0 = self.Export(self.conn.pins.request('14').adapt_to(din_model)) - self.bs1 = self.Export(self.conn.pins.request('13').adapt_to(din_model)) - self.connect(self.conn.pins.request('12').adapt_to(Ground()), self.vss) # BS2, 0 for any serial + self.bs0 = self.Export(self.conn.pins.request("14").adapt_to(din_model)) + self.bs1 = self.Export(self.conn.pins.request("13").adapt_to(din_model)) + self.connect(self.conn.pins.request("12").adapt_to(Ground()), self.vss) # BS2, 0 for any serial - self.res = self.Export(self.conn.pins.request('9').adapt_to(din_model)) - self.cs = self.Export(self.conn.pins.request('11').adapt_to(din_model)) - self.dc = self.Export(self.conn.pins.request('10').adapt_to(din_model)) + self.res = self.Export(self.conn.pins.request("9").adapt_to(din_model)) + self.cs = self.Export(self.conn.pins.request("11").adapt_to(din_model)) + self.dc = self.Export(self.conn.pins.request("10").adapt_to(din_model)) - self.d0 = self.Export(self.conn.pins.request('18').adapt_to(din_model)) - self.d1 = self.Export(self.conn.pins.request('19').adapt_to(din_model)) - self.d2 = self.Export(self.conn.pins.request('20').adapt_to(din_model), optional=True) + self.d0 = self.Export(self.conn.pins.request("18").adapt_to(din_model)) + self.d1 = self.Export(self.conn.pins.request("19").adapt_to(din_model)) + self.d2 = self.Export(self.conn.pins.request("20").adapt_to(din_model), optional=True) for i in [16, 15] + list(range(21, 26)): # RD, RW, DB3~DB7 self.connect(vss_pin, self.conn.pins.request(str(i))) @@ -79,6 +104,7 @@ def __init__(self) -> None: class Er_Oled_096_1c(Oled, Resettable, GeneratorBlock): """SSD1357-based 0.96" 128x64 RGB OLED, in either I2C or SPI mode.""" + def __init__(self) -> None: super().__init__() self.device = self.Block(Er_Oled_096_1c_Device()) @@ -99,20 +125,23 @@ def contents(self) -> None: self.lcd = self.Block(Er_Oled_096_1c_Outline()) # for device outline - self.iref_res = self.Block(Resistor(resistance=1*MOhm(tol=0.05))) # TODO dynamic sizing + self.iref_res = self.Block(Resistor(resistance=1 * MOhm(tol=0.05))) # TODO dynamic sizing self.connect(self.iref_res.a, self.device.iref) self.connect(self.iref_res.b.adapt_to(Ground()), self.gnd) - self.vcomh_cap = self.Block(DecouplingCapacitor(4.7*uFarad(tol=0.2))).connected(self.gnd, self.device.vcomh) - self.vp_cap = self.Block(DecouplingCapacitor(1*uFarad(tol=0.2))).connected(self.gnd, self.device.vp) + self.vcomh_cap = self.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(self.gnd, self.device.vcomh) + self.vp_cap = self.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))).connected(self.gnd, self.device.vp) - self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vdd) - self.vcc_cap = self.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.2)))\ - .connected(self.gnd, self.device.vcc) + self.vdd_cap = self.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vdd + ) + self.vcc_cap = self.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.2))).connected( + self.gnd, self.device.vcc + ) - self.vsl_res = self.Block(Resistor(resistance=50*Ohm(tol=0.05))) - diode_model = Diode(reverse_voltage=(0, 0)*Volt, current=(0, 0)*Amp, - voltage_drop=(0, 0.8)*Volt) # arbitrary tolerance, reference is 0.7V + self.vsl_res = self.Block(Resistor(resistance=50 * Ohm(tol=0.05))) + diode_model = Diode( + reverse_voltage=(0, 0) * Volt, current=(0, 0) * Amp, voltage_drop=(0, 0.8) * Volt + ) # arbitrary tolerance, reference is 0.7V self.vsl_d1 = self.Block(diode_model) self.vsl_d2 = self.Block(diode_model) self.connect(self.device.vsl, self.vsl_res.a) @@ -127,7 +156,9 @@ def generate(self) -> None: gnd_digital = self.gnd.as_digital_source() if self.get(self.i2c.is_connected()): - pwr_digital = self.pwr.as_digital_source() # workaround for issue #259: if this is never used it creates a broken empty adapter + pwr_digital = ( + self.pwr.as_digital_source() + ) # workaround for issue #259: if this is never used it creates a broken empty adapter self.connect(self.device.bs0, gnd_digital) self.connect(self.device.bs1, pwr_digital) @@ -136,7 +167,7 @@ def generate(self) -> None: self.connect(self.device.dc, gnd_digital) # addr, TODO support I2C addr self.connect(self.device.cs, gnd_digital) self.require(~self.spi.is_connected() & ~self.cs.is_connected() & ~self.dc.is_connected()) - self.assign(self.i2c.addresses, [0x3c]) + self.assign(self.i2c.addresses, [0x3C]) elif self.get(self.spi.is_connected()): self.connect(self.device.bs1, gnd_digital) self.connect(self.spi.sck, self.device.d0) diff --git a/edg/parts/Oled_Nhd_312_25664uc.py b/edg/parts/Oled_Nhd_312_25664uc.py index 393e9da2e..4805a86ea 100644 --- a/edg/parts/Oled_Nhd_312_25664uc.py +++ b/edg/parts/Oled_Nhd_312_25664uc.py @@ -6,80 +6,82 @@ class Nhd_312_25664uc_Device(InternalSubcircuit, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.vdd = self.Port(VoltageSink( - voltage_limits=(3*0.95, 3.3) * Volt, # Chapter 5, no minimum given, assuming a -5% tolerance - current_draw=(0.003, 290) * mAmp # Chapter 5, typ sleep current to 100% brightness at 3v - )) - self.vss = self.Port(Ground()) - - io_model = DigitalBidir.from_supply( - self.vss, self.vdd, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # not stated, assumed - current_draw=0*mAmp(tol=0), # not stated, assumed - current_limits=0*mAmp(tol=0), # not stated, assumed - input_threshold_factor=(0.2, 0.8), - output_threshold_factor=(0.1, 0.9) - ) - self.dc = self.Port(DigitalSink.from_bidir(io_model)) - self.sclk = self.Port(DigitalSink.from_bidir(io_model)) - self.sdin = self.Port(DigitalSink.from_bidir(io_model)) - - self.nres = self.Port(DigitalSink.from_bidir(io_model)) - self.ncs = self.Port(DigitalSink.from_bidir(io_model)) - - @override - def contents(self) -> None: - super().contents() - - pinning: Dict[str, CircuitPort] = { - '1': self.vss, - '2': self.vdd, - # '3': # no connect - '4': self.dc, - '5': self.vss, # R/nW in parallel interface - '6': self.vss, # E/nRD in parallel interface - '7': self.sclk, - '8': self.sdin, - # '9' # no connect # DB2 in parallel interface - '10': self.vss, # DB3 in parallel interface - '11': self.vss, # DB4 in parallel interface - '12': self.vss, # DB5 in parallel interface - '13': self.vss, # DB6 in parallel interface - '14': self.vss, # DB7 in parallel interface - # '15': # no connect - '16': self.nres, - '17': self.ncs, - # '18': # no connect - '19': self.vss, # BS1 parallel (1) / serial (0) mode - '20': self.vss, # BS0 4-wire serial mode - } - - self.footprint( - 'U', 'calisco:NHD-3.12-25664UCB2', - pinning, - part='20-pin header, NHD-3.12-25664UCB2' # TODO multiple parts - ) + def __init__(self) -> None: + super().__init__() + + self.vdd = self.Port( + VoltageSink( + voltage_limits=(3 * 0.95, 3.3) * Volt, # Chapter 5, no minimum given, assuming a -5% tolerance + current_draw=(0.003, 290) * mAmp, # Chapter 5, typ sleep current to 100% brightness at 3v + ) + ) + self.vss = self.Port(Ground()) + + io_model = DigitalBidir.from_supply( + self.vss, + self.vdd, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, # not stated, assumed + current_draw=0 * mAmp(tol=0), # not stated, assumed + current_limits=0 * mAmp(tol=0), # not stated, assumed + input_threshold_factor=(0.2, 0.8), + output_threshold_factor=(0.1, 0.9), + ) + self.dc = self.Port(DigitalSink.from_bidir(io_model)) + self.sclk = self.Port(DigitalSink.from_bidir(io_model)) + self.sdin = self.Port(DigitalSink.from_bidir(io_model)) + + self.nres = self.Port(DigitalSink.from_bidir(io_model)) + self.ncs = self.Port(DigitalSink.from_bidir(io_model)) + + @override + def contents(self) -> None: + super().contents() + + pinning: Dict[str, CircuitPort] = { + "1": self.vss, + "2": self.vdd, + # '3': # no connect + "4": self.dc, + "5": self.vss, # R/nW in parallel interface + "6": self.vss, # E/nRD in parallel interface + "7": self.sclk, + "8": self.sdin, + # '9' # no connect # DB2 in parallel interface + "10": self.vss, # DB3 in parallel interface + "11": self.vss, # DB4 in parallel interface + "12": self.vss, # DB5 in parallel interface + "13": self.vss, # DB6 in parallel interface + "14": self.vss, # DB7 in parallel interface + # '15': # no connect + "16": self.nres, + "17": self.ncs, + # '18': # no connect + "19": self.vss, # BS1 parallel (1) / serial (0) mode + "20": self.vss, # BS0 4-wire serial mode + } + + self.footprint( + "U", "calisco:NHD-3.12-25664UCB2", pinning, part="20-pin header, NHD-3.12-25664UCB2" # TODO multiple parts + ) class Nhd_312_25664uc(Oled, Block): - """256x64 3.12" passive-matrix OLED""" - def __init__(self) -> None: - super().__init__() - - self.device = self.Block(Nhd_312_25664uc_Device()) - self.gnd = self.Export(self.device.vss, [Common]) - self.pwr = self.Export(self.device.vdd, [Power]) - self.reset = self.Export(self.device.nres) - self.dc = self.Export(self.device.dc) - self.cs = self.Export(self.device.ncs) - self.spi = self.Port(SpiPeripheral(DigitalBidir.empty())) - - @override - def contents(self) -> None: - super().contents() - - self.connect(self.spi.sck, self.device.sclk) - self.connect(self.spi.mosi, self.device.sdin) + """256x64 3.12" passive-matrix OLED""" + + def __init__(self) -> None: + super().__init__() + + self.device = self.Block(Nhd_312_25664uc_Device()) + self.gnd = self.Export(self.device.vss, [Common]) + self.pwr = self.Export(self.device.vdd, [Power]) + self.reset = self.Export(self.device.nres) + self.dc = self.Export(self.device.dc) + self.cs = self.Export(self.device.ncs) + self.spi = self.Port(SpiPeripheral(DigitalBidir.empty())) + + @override + def contents(self) -> None: + super().contents() + + self.connect(self.spi.sck, self.device.sclk) + self.connect(self.spi.mosi, self.device.sdin) diff --git a/edg/parts/Opamp_Lmv321.py b/edg/parts/Opamp_Lmv321.py index 774dd5696..08bcf9ef7 100644 --- a/edg/parts/Opamp_Lmv321.py +++ b/edg/parts/Opamp_Lmv321.py @@ -5,60 +5,68 @@ class Lmv321_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.vcc = self.Port(VoltageSink( - voltage_limits=(2.7, 5.5)*Volt, current_draw=(80, 170)*uAmp # quiescent current - ), [Power]) - self.vss = self.Port(Ground(), [Common]) + def __init__(self) -> None: + super().__init__() + self.vcc = self.Port( + VoltageSink(voltage_limits=(2.7, 5.5) * Volt, current_draw=(80, 170) * uAmp), [Power] # quiescent current + ) + self.vss = self.Port(Ground(), [Common]) - analog_in_model = AnalogSink.from_supply( - self.vss, self.vcc, - voltage_limit_abs=(-0.2, 5.7), - signal_limit_bound=(0, -1.0*Volt), # assumed, from Vcc = 2.7v and 5v tables - ) - self.vinp = self.Port(analog_in_model) - self.vinn = self.Port(analog_in_model) - self.vout = self.Port(AnalogSource.from_supply( - self.vss, self.vcc, - signal_out_bound=(0.180*Volt, -0.100*Volt), # assuming a 10k load, Vcc=2.7v - current_limits=(-40, 40)*mAmp, # output short circuit current - )) + analog_in_model = AnalogSink.from_supply( + self.vss, + self.vcc, + voltage_limit_abs=(-0.2, 5.7), + signal_limit_bound=(0, -1.0 * Volt), # assumed, from Vcc = 2.7v and 5v tables + ) + self.vinp = self.Port(analog_in_model) + self.vinn = self.Port(analog_in_model) + self.vout = self.Port( + AnalogSource.from_supply( + self.vss, + self.vcc, + signal_out_bound=(0.180 * Volt, -0.100 * Volt), # assuming a 10k load, Vcc=2.7v + current_limits=(-40, 40) * mAmp, # output short circuit current + ) + ) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.vinp, - '2': self.vss, - '3': self.vinn, - '4': self.vout, - '5': self.vcc, - }, - mfr='Texas Instruments', part='LMV321', - datasheet='https://www.ti.com/lit/ds/symlink/lmv321.pdf' - ) - self.assign(self.lcsc_part, 'C7972') - self.assign(self.actual_basic_part, True) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.vinp, + "2": self.vss, + "3": self.vinn, + "4": self.vout, + "5": self.vcc, + }, + mfr="Texas Instruments", + part="LMV321", + datasheet="https://www.ti.com/lit/ds/symlink/lmv321.pdf", + ) + self.assign(self.lcsc_part, "C7972") + self.assign(self.actual_basic_part, True) class Lmv321(Opamp): - """RRO op-amp in SOT-23-5. - """ - @override - def contents(self) -> None: - super().contents() + """RRO op-amp in SOT-23-5.""" - self.ic = self.Block(Lmv321_Device()) - self.connect(self.inn, self.ic.vinn) - self.connect(self.inp, self.ic.vinp) - self.connect(self.out, self.ic.vout) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.vss) + @override + def contents(self) -> None: + super().contents() - # Datasheet section 9: place 0.1uF bypass capacitors close to the power-supply pins - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + self.ic = self.Block(Lmv321_Device()) + self.connect(self.inn, self.ic.vinn) + self.connect(self.inp, self.ic.vinp) + self.connect(self.out, self.ic.vout) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.vss) + + # Datasheet section 9: place 0.1uF bypass capacitors close to the power-supply pins + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) diff --git a/edg/parts/Opamp_Mcp6001.py b/edg/parts/Opamp_Mcp6001.py index 7df44c745..2f86e2953 100644 --- a/edg/parts/Opamp_Mcp6001.py +++ b/edg/parts/Opamp_Mcp6001.py @@ -5,61 +5,63 @@ class Mcp6001_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vcc = self.Port(VoltageSink( - voltage_limits=(1.8, 6.0)*Volt, current_draw=(50, 170)*uAmp - ), [Power]) - self.vss = self.Port(Ground(), [Common]) + def __init__(self) -> None: + super().__init__() + self.vcc = self.Port(VoltageSink(voltage_limits=(1.8, 6.0) * Volt, current_draw=(50, 170) * uAmp), [Power]) + self.vss = self.Port(Ground(), [Common]) - analog_in_model = AnalogSink.from_supply( - self.vss, self.vcc, - voltage_limit_tolerance=(-1.0, 1.0)*Volt, # absolute maximum ratings - signal_limit_bound=(0.3*Volt, -0.3*Volt), # common-mode input range - impedance=1e13*Ohm(tol=0), # no tolerance bounds given on datasheet - ) - self.vinp = self.Port(analog_in_model) - self.vinn = self.Port(analog_in_model) - self.vout = self.Port(AnalogSource.from_supply( - self.vss, self.vcc, - signal_out_bound=(25*mVolt, -25*mVolt), # maximum output swing - current_limits=(-6, 6)*mAmp, # for Vdd=1.8, 23mA for Vdd=5.5 - impedance=300*Ohm(tol=0) # no tolerance bounds given on datasheet - )) + analog_in_model = AnalogSink.from_supply( + self.vss, + self.vcc, + voltage_limit_tolerance=(-1.0, 1.0) * Volt, # absolute maximum ratings + signal_limit_bound=(0.3 * Volt, -0.3 * Volt), # common-mode input range + impedance=1e13 * Ohm(tol=0), # no tolerance bounds given on datasheet + ) + self.vinp = self.Port(analog_in_model) + self.vinn = self.Port(analog_in_model) + self.vout = self.Port( + AnalogSource.from_supply( + self.vss, + self.vcc, + signal_out_bound=(25 * mVolt, -25 * mVolt), # maximum output swing + current_limits=(-6, 6) * mAmp, # for Vdd=1.8, 23mA for Vdd=5.5 + impedance=300 * Ohm(tol=0), # no tolerance bounds given on datasheet + ) + ) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-5', - { - '1': self.vout, - '2': self.vss, - '3': self.vinp, - '4': self.vinn, - '5': self.vcc, - }, - mfr='Microchip Technology', part='MCP6001T-I/OT', - datasheet='https://ww1.microchip.com/downloads/en/DeviceDoc/21733j.pdf' - ) - self.assign(self.lcsc_part, 'C116490') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-5", + { + "1": self.vout, + "2": self.vss, + "3": self.vinp, + "4": self.vinn, + "5": self.vcc, + }, + mfr="Microchip Technology", + part="MCP6001T-I/OT", + datasheet="https://ww1.microchip.com/downloads/en/DeviceDoc/21733j.pdf", + ) + self.assign(self.lcsc_part, "C116490") + self.assign(self.actual_basic_part, False) class Mcp6001(Opamp): - """MCP6001 RRO op-amp in SOT-23-5 - """ - @override - def contents(self) -> None: - super().contents() + """MCP6001 RRO op-amp in SOT-23-5""" - self.ic = self.Block(Mcp6001_Device()) - self.connect(self.inn, self.ic.vinn) - self.connect(self.inp, self.ic.vinp) - self.connect(self.out, self.ic.vout) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.vss) + @override + def contents(self) -> None: + super().contents() - self.vdd_cap0 = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2) - )).connected(self.gnd, self.pwr) + self.ic = self.Block(Mcp6001_Device()) + self.connect(self.inn, self.ic.vinn) + self.connect(self.inp, self.ic.vinp) + self.connect(self.out, self.ic.vout) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.vss) + + self.vdd_cap0 = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/Opamp_Opax171.py b/edg/parts/Opamp_Opax171.py index 472d220cd..3ce352132 100644 --- a/edg/parts/Opamp_Opax171.py +++ b/edg/parts/Opamp_Opax171.py @@ -6,82 +6,95 @@ @non_library class Opa171_Base_Device(InternalSubcircuit): - DEVICES: int + DEVICES: int - def _analog_in_model(self) -> AnalogSink: - return AnalogSink.from_supply( - self.vn, self.vp, - voltage_limit_tolerance=(-0.5, 0.5)*Volt, # input common mode absolute maximum ratings - signal_limit_bound=(-0.1*Volt, -2*Volt), - impedance=100e6*Ohm(tol=0) # no tolerance specified; differential impedance - ) + def _analog_in_model(self) -> AnalogSink: + return AnalogSink.from_supply( + self.vn, + self.vp, + voltage_limit_tolerance=(-0.5, 0.5) * Volt, # input common mode absolute maximum ratings + signal_limit_bound=(-0.1 * Volt, -2 * Volt), + impedance=100e6 * Ohm(tol=0), # no tolerance specified; differential impedance + ) - def _analog_out_model(self) -> AnalogSource: - return AnalogSource.from_supply( - self.vn, self.vp, - signal_out_bound=(0.350*Volt, -0.350*Volt), # output swing from rail, 10k load, over temperature - current_limits=(-35, 25)*mAmp, # short circuit current - impedance=150*Ohm(tol=0) # open-loop resistance - ) + def _analog_out_model(self) -> AnalogSource: + return AnalogSource.from_supply( + self.vn, + self.vp, + signal_out_bound=(0.350 * Volt, -0.350 * Volt), # output swing from rail, 10k load, over temperature + current_limits=(-35, 25) * mAmp, # short circuit current + impedance=150 * Ohm(tol=0), # open-loop resistance + ) - def __init__(self) -> None: - super().__init__() - self.vn = self.Port(Ground(), [Common]) - self.vp = self.Port(VoltageSink( - voltage_limits=(4.5, 36)*Volt, - current_draw=(475 * self.DEVICES, 650 * self.DEVICES)*uAmp # quiescent current - ), [Power]) + def __init__(self) -> None: + super().__init__() + self.vn = self.Port(Ground(), [Common]) + self.vp = self.Port( + VoltageSink( + voltage_limits=(4.5, 36) * Volt, + current_draw=(475 * self.DEVICES, 650 * self.DEVICES) * uAmp, # quiescent current + ), + [Power], + ) class Opa2171_Device(Opa171_Base_Device, JlcPart, FootprintBlock): - DEVICES = 2 + DEVICES = 2 - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - analog_in_model = self._analog_in_model() - analog_out_model = self._analog_out_model() - self.inpa = self.Port(analog_in_model) - self.inna = self.Port(analog_in_model) - self.outa = self.Port(analog_out_model) - self.inpb = self.Port(analog_in_model) - self.innb = self.Port(analog_in_model) - self.outb = self.Port(analog_out_model) + analog_in_model = self._analog_in_model() + analog_out_model = self._analog_out_model() + self.inpa = self.Port(analog_in_model) + self.inna = self.Port(analog_in_model) + self.outa = self.Port(analog_out_model) + self.inpb = self.Port(analog_in_model) + self.innb = self.Port(analog_in_model) + self.outb = self.Port(analog_out_model) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.outa, - '2': self.inna, - '3': self.inpa, - '4': self.vn, - '5': self.inpb, - '6': self.innb, - '7': self.outb, - '8': self.vp, - }, - mfr='Texas Instruments', part='OPA2171AIDR', - datasheet='https://www.ti.com/lit/ds/symlink/opa2171.pdf' - ) - self.assign(self.lcsc_part, 'C40904') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.outa, + "2": self.inna, + "3": self.inpa, + "4": self.vn, + "5": self.inpb, + "6": self.innb, + "7": self.outb, + "8": self.vp, + }, + mfr="Texas Instruments", + part="OPA2171AIDR", + datasheet="https://www.ti.com/lit/ds/symlink/opa2171.pdf", + ) + self.assign(self.lcsc_part, "C40904") + self.assign(self.actual_basic_part, False) class Opa2171(MultipackOpampGenerator): - """Dual precision general purpose RRO opamp. - """ - @override - def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: - self.ic = self.Block(Opa2171_Device()) - # Datasheet section 9: recommend 0.1uF bypass capacitors close to power supply pins - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.ic.vn, self.ic.vp) + """Dual precision general purpose RRO opamp.""" - return self.OpampPorts(self.ic.vn, self.ic.vp, [ - (self.ic.inna, self.ic.inpa, self.ic.outa), - (self.ic.innb, self.ic.inpb, self.ic.outb), - ]) + @override + def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: + self.ic = self.Block(Opa2171_Device()) + # Datasheet section 9: recommend 0.1uF bypass capacitors close to power supply pins + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.ic.vn, self.ic.vp) + + return self.OpampPorts( + self.ic.vn, + self.ic.vp, + [ + (self.ic.inna, self.ic.inpa, self.ic.outa), + (self.ic.innb, self.ic.inpb, self.ic.outb), + ], + ) diff --git a/edg/parts/Opamp_Opax189.py b/edg/parts/Opamp_Opax189.py index 83a582599..a58dbacd7 100644 --- a/edg/parts/Opamp_Opax189.py +++ b/edg/parts/Opamp_Opax189.py @@ -6,133 +6,150 @@ @non_library class Opax189_Base_Device(InternalSubcircuit): - DEVICES: int - - def _analog_in_model(self) -> AnalogSink: - return AnalogSink.from_supply( - self.vn, self.vp, - voltage_limit_tolerance=(-0.5, 0.5)*Volt, # input common mode absolute maximum ratings - signal_limit_bound=(-0.1, -2.5), - impedance=100*MOhm(tol=0), # differential input impedance; no tolerance bounds specified - ) - - def _analog_out_model(self) -> AnalogSource: - return AnalogSource.from_supply( - self.vn, self.vp, - signal_out_bound=(0.110*Volt, -0.111*Volt), # output swing from rail, assumed at 10k load - current_limits=(-65, 65)*mAmp, # for +/-18V supply - impedance=380*Ohm(tol=0) # open-loop impedance; no tolerance bounds specified - ) - - def __init__(self) -> None: - super().__init__() - self.vn = self.Port(Ground(), [Common]) - self.vp = self.Port(VoltageSink( - voltage_limits=(4.5, 36)*Volt, - current_draw=(1 * self.DEVICES, 1.5 * self.DEVICES)*mAmp # quiescent current - ), [Power]) + DEVICES: int + + def _analog_in_model(self) -> AnalogSink: + return AnalogSink.from_supply( + self.vn, + self.vp, + voltage_limit_tolerance=(-0.5, 0.5) * Volt, # input common mode absolute maximum ratings + signal_limit_bound=(-0.1, -2.5), + impedance=100 * MOhm(tol=0), # differential input impedance; no tolerance bounds specified + ) + + def _analog_out_model(self) -> AnalogSource: + return AnalogSource.from_supply( + self.vn, + self.vp, + signal_out_bound=(0.110 * Volt, -0.111 * Volt), # output swing from rail, assumed at 10k load + current_limits=(-65, 65) * mAmp, # for +/-18V supply + impedance=380 * Ohm(tol=0), # open-loop impedance; no tolerance bounds specified + ) + + def __init__(self) -> None: + super().__init__() + self.vn = self.Port(Ground(), [Common]) + self.vp = self.Port( + VoltageSink( + voltage_limits=(4.5, 36) * Volt, + current_draw=(1 * self.DEVICES, 1.5 * self.DEVICES) * mAmp, # quiescent current + ), + [Power], + ) class Opa189_Device(Opax189_Base_Device, JlcPart, FootprintBlock): - DEVICES = 1 - - def __init__(self) -> None: - super().__init__() - analog_in_model = self._analog_in_model() - self.vinp = self.Port(analog_in_model) - self.vinn = self.Port(analog_in_model) - self.vout = self.Port(self._analog_out_model()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - # 1 is NC - '2': self.vinn, - '3': self.vinp, - '4': self.vn, - # 5 is NC - '6': self.vout, - '7': self.vp, - # 8 is NC - }, - mfr='Texas Instruments', part='OPA189DR', - datasheet='https://www.ti.com/lit/ds/symlink/opa4189.pdf' - ) - self.assign(self.lcsc_part, 'C781811') - self.assign(self.actual_basic_part, False) + DEVICES = 1 + + def __init__(self) -> None: + super().__init__() + analog_in_model = self._analog_in_model() + self.vinp = self.Port(analog_in_model) + self.vinn = self.Port(analog_in_model) + self.vout = self.Port(self._analog_out_model()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + # 1 is NC + "2": self.vinn, + "3": self.vinp, + "4": self.vn, + # 5 is NC + "6": self.vout, + "7": self.vp, + # 8 is NC + }, + mfr="Texas Instruments", + part="OPA189DR", + datasheet="https://www.ti.com/lit/ds/symlink/opa4189.pdf", + ) + self.assign(self.lcsc_part, "C781811") + self.assign(self.actual_basic_part, False) class Opa189(Opamp): - """High voltage (4.5-36V), low-noise opamp in SOIC-8. - """ - @override - def contents(self) -> None: - super().contents() + """High voltage (4.5-36V), low-noise opamp in SOIC-8.""" - self.ic = self.Block(Opa189_Device()) - self.connect(self.inn, self.ic.vinn) - self.connect(self.inp, self.ic.vinp) - self.connect(self.out, self.ic.vout) - self.connect(self.pwr, self.ic.vp) - self.connect(self.gnd, self.ic.vn) + @override + def contents(self) -> None: + super().contents() - # Datasheet section 10: use a low-ESR 0.1uF capacitor between each supply and ground pin - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + self.ic = self.Block(Opa189_Device()) + self.connect(self.inn, self.ic.vinn) + self.connect(self.inp, self.ic.vinp) + self.connect(self.out, self.ic.vout) + self.connect(self.pwr, self.ic.vp) + self.connect(self.gnd, self.ic.vn) + + # Datasheet section 10: use a low-ESR 0.1uF capacitor between each supply and ground pin + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) class Opa2189_Device(Opax189_Base_Device, JlcPart, FootprintBlock): - DEVICES = 2 - - def __init__(self) -> None: - super().__init__() - analog_in_model = self._analog_in_model() - analog_out_model = self._analog_out_model() - self.inpa = self.Port(analog_in_model) - self.inna = self.Port(analog_in_model) - self.outa = self.Port(analog_out_model) - self.inpb = self.Port(analog_in_model) - self.innb = self.Port(analog_in_model) - self.outb = self.Port(analog_out_model) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.outa, - '2': self.inna, - '3': self.inpa, - '4': self.vn, - '5': self.inpb, - '6': self.innb, - '7': self.outb, - '8': self.vp, - }, - mfr='Texas Instruments', part='OPA2197IDR', - datasheet='https://www.ti.com/lit/ds/symlink/opa4189.pdf' - ) - self.assign(self.lcsc_part, 'C881489') - self.assign(self.actual_basic_part, False) + DEVICES = 2 + + def __init__(self) -> None: + super().__init__() + analog_in_model = self._analog_in_model() + analog_out_model = self._analog_out_model() + self.inpa = self.Port(analog_in_model) + self.inna = self.Port(analog_in_model) + self.outa = self.Port(analog_out_model) + self.inpb = self.Port(analog_in_model) + self.innb = self.Port(analog_in_model) + self.outb = self.Port(analog_out_model) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.outa, + "2": self.inna, + "3": self.inpa, + "4": self.vn, + "5": self.inpb, + "6": self.innb, + "7": self.outb, + "8": self.vp, + }, + mfr="Texas Instruments", + part="OPA2197IDR", + datasheet="https://www.ti.com/lit/ds/symlink/opa4189.pdf", + ) + self.assign(self.lcsc_part, "C881489") + self.assign(self.actual_basic_part, False) class Opa2189(MultipackOpampGenerator): - """Dual precision RRO opamps. - """ - @override - def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: - self.ic = self.Block(Opa2189_Device()) - # Datasheet section 10: recommend 0.1uF bypass capacitors close to power supply pins - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.ic.vn, self.ic.vp) - - return self.OpampPorts(self.ic.vn, self.ic.vp, [ - (self.ic.inna, self.ic.inpa, self.ic.outa), - (self.ic.innb, self.ic.inpb, self.ic.outb), - ]) + """Dual precision RRO opamps.""" + + @override + def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: + self.ic = self.Block(Opa2189_Device()) + # Datasheet section 10: recommend 0.1uF bypass capacitors close to power supply pins + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.ic.vn, self.ic.vp) + + return self.OpampPorts( + self.ic.vn, + self.ic.vp, + [ + (self.ic.inna, self.ic.inpa, self.ic.outa), + (self.ic.innb, self.ic.inpb, self.ic.outb), + ], + ) diff --git a/edg/parts/Opamp_Opax197.py b/edg/parts/Opamp_Opax197.py index e96098170..7c424ed8b 100644 --- a/edg/parts/Opamp_Opax197.py +++ b/edg/parts/Opamp_Opax197.py @@ -6,135 +6,153 @@ @non_library class Opa197_Base_Device(InternalSubcircuit): - DEVICES: int - - def _analog_in_model(self) -> AnalogSink: - return AnalogSink.from_supply( - self.vn, self.vp, - voltage_limit_tolerance=(-0.5, 0.5)*Volt, # input common mode absolute maximum ratings - signal_limit_tolerance=(-0.1, 0.1)*Volt, - impedance=1e13*Ohm(tol=0) # no tolerance bounds given on datasheet - ) - - def _analog_out_model(self) -> AnalogSource: - return AnalogSource.from_supply( - self.vn, self.vp, - signal_out_bound=(0.125*Volt, -0.125*Volt), # output swing from rail, assumed at 10k load - current_limits=(-65, 65)*mAmp, # for +/-18V supply - impedance=375*Ohm(tol=0) # no tolerance bounds given on datasheet; open-loop impedance - ) - - def __init__(self) -> None: - super().__init__() - self.vn = self.Port(Ground(), [Common]) - self.vp = self.Port(VoltageSink( - voltage_limits=(4.5, 36)*Volt, - current_draw=(1 * self.DEVICES, 1.5 * self.DEVICES)*mAmp # quiescent current - ), [Power]) + DEVICES: int + + def _analog_in_model(self) -> AnalogSink: + return AnalogSink.from_supply( + self.vn, + self.vp, + voltage_limit_tolerance=(-0.5, 0.5) * Volt, # input common mode absolute maximum ratings + signal_limit_tolerance=(-0.1, 0.1) * Volt, + impedance=1e13 * Ohm(tol=0), # no tolerance bounds given on datasheet + ) + + def _analog_out_model(self) -> AnalogSource: + return AnalogSource.from_supply( + self.vn, + self.vp, + signal_out_bound=(0.125 * Volt, -0.125 * Volt), # output swing from rail, assumed at 10k load + current_limits=(-65, 65) * mAmp, # for +/-18V supply + impedance=375 * Ohm(tol=0), # no tolerance bounds given on datasheet; open-loop impedance + ) + + def __init__(self) -> None: + super().__init__() + self.vn = self.Port(Ground(), [Common]) + self.vp = self.Port( + VoltageSink( + voltage_limits=(4.5, 36) * Volt, + current_draw=(1 * self.DEVICES, 1.5 * self.DEVICES) * mAmp, # quiescent current + ), + [Power], + ) class Opa197_Device(Opa197_Base_Device, JlcPart, FootprintBlock): - DEVICES = 1 - - def __init__(self) -> None: - super().__init__() - analog_in_model = self._analog_in_model() - self.vinp = self.Port(analog_in_model) - self.vinn = self.Port(analog_in_model) - self.vout = self.Port(self._analog_out_model()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - # 1 is NC - '2': self.vinn, - '3': self.vinp, - '4': self.vn, - # 5 is NC - '6': self.vout, - '7': self.vp, - # 8 is NC - }, - mfr='Texas Instruments', part='OPA197IDR', - datasheet='https://www.ti.com/lit/ds/symlink/opa197.pdf' - ) - self.assign(self.lcsc_part, 'C79274') - self.assign(self.actual_basic_part, False) + DEVICES = 1 + + def __init__(self) -> None: + super().__init__() + analog_in_model = self._analog_in_model() + self.vinp = self.Port(analog_in_model) + self.vinn = self.Port(analog_in_model) + self.vout = self.Port(self._analog_out_model()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + # 1 is NC + "2": self.vinn, + "3": self.vinp, + "4": self.vn, + # 5 is NC + "6": self.vout, + "7": self.vp, + # 8 is NC + }, + mfr="Texas Instruments", + part="OPA197IDR", + datasheet="https://www.ti.com/lit/ds/symlink/opa197.pdf", + ) + self.assign(self.lcsc_part, "C79274") + self.assign(self.actual_basic_part, False) class Opa197(Opamp): - """High voltage opamp (4.5-36V) in SOIC-8. - (part also available in SOT-23-5) - """ - @override - def contents(self) -> None: - super().contents() - - self.ic = self.Block(Opa197_Device()) - self.connect(self.inn, self.ic.vinn) - self.connect(self.inp, self.ic.vinp) - self.connect(self.out, self.ic.vout) - self.connect(self.pwr, self.ic.vp) - self.connect(self.gnd, self.ic.vn) - - # Datasheet section 10.1: use a low-ESR 0.1uF capacitor between each supply and ground pin - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + """High voltage opamp (4.5-36V) in SOIC-8. + (part also available in SOT-23-5) + """ + + @override + def contents(self) -> None: + super().contents() + + self.ic = self.Block(Opa197_Device()) + self.connect(self.inn, self.ic.vinn) + self.connect(self.inp, self.ic.vinp) + self.connect(self.out, self.ic.vout) + self.connect(self.pwr, self.ic.vp) + self.connect(self.gnd, self.ic.vn) + + # Datasheet section 10.1: use a low-ESR 0.1uF capacitor between each supply and ground pin + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) class Opa2197_Device(Opa197_Base_Device, JlcPart, FootprintBlock): - DEVICES = 2 - - def __init__(self) -> None: - super().__init__() - - analog_in_model = self._analog_in_model() - analog_out_model = self._analog_out_model() - self.inpa = self.Port(analog_in_model) - self.inna = self.Port(analog_in_model) - self.outa = self.Port(analog_out_model) - self.inpb = self.Port(analog_in_model) - self.innb = self.Port(analog_in_model) - self.outb = self.Port(analog_out_model) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.outa, - '2': self.inna, - '3': self.inpa, - '4': self.vn, - '5': self.inpb, - '6': self.innb, - '7': self.outb, - '8': self.vp, - }, - mfr='Texas Instruments', part='OPA2197IDR', - datasheet='https://www.ti.com/lit/ds/symlink/opa197.pdf' - ) - self.assign(self.lcsc_part, 'C139363') - self.assign(self.actual_basic_part, False) + DEVICES = 2 + + def __init__(self) -> None: + super().__init__() + + analog_in_model = self._analog_in_model() + analog_out_model = self._analog_out_model() + self.inpa = self.Port(analog_in_model) + self.inna = self.Port(analog_in_model) + self.outa = self.Port(analog_out_model) + self.inpb = self.Port(analog_in_model) + self.innb = self.Port(analog_in_model) + self.outb = self.Port(analog_out_model) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.outa, + "2": self.inna, + "3": self.inpa, + "4": self.vn, + "5": self.inpb, + "6": self.innb, + "7": self.outb, + "8": self.vp, + }, + mfr="Texas Instruments", + part="OPA2197IDR", + datasheet="https://www.ti.com/lit/ds/symlink/opa197.pdf", + ) + self.assign(self.lcsc_part, "C139363") + self.assign(self.actual_basic_part, False) class Opa2197(MultipackOpampGenerator): - """Dual precision RRO opamps. - """ - @override - def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: - self.ic = self.Block(Opa2197_Device()) - # Datasheet section 9: recommend 0.1uF bypass capacitors close to power supply pins - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.ic.vn, self.ic.vp) - - return self.OpampPorts(self.ic.vn, self.ic.vp, [ - (self.ic.inna, self.ic.inpa, self.ic.outa), - (self.ic.innb, self.ic.inpb, self.ic.outb), - ]) + """Dual precision RRO opamps.""" + + @override + def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: + self.ic = self.Block(Opa2197_Device()) + # Datasheet section 9: recommend 0.1uF bypass capacitors close to power supply pins + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.ic.vn, self.ic.vp) + + return self.OpampPorts( + self.ic.vn, + self.ic.vp, + [ + (self.ic.inna, self.ic.inpa, self.ic.outa), + (self.ic.innb, self.ic.inpb, self.ic.outb), + ], + ) diff --git a/edg/parts/Opamp_Opax333.py b/edg/parts/Opamp_Opax333.py index f847d0a5a..e5d4eaaf8 100644 --- a/edg/parts/Opamp_Opax333.py +++ b/edg/parts/Opamp_Opax333.py @@ -5,66 +5,80 @@ class Opa2333_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vp = self.Port(VoltageSink( - voltage_limits=(1.8, 5.5)*Volt, current_draw=(17*2, 28*2)*uAmp # quiescent current for both amps - ), [Power]) - self.vn = self.Port(Ground(), [Common]) + def __init__(self) -> None: + super().__init__() + self.vp = self.Port( + VoltageSink( + voltage_limits=(1.8, 5.5) * Volt, + current_draw=(17 * 2, 28 * 2) * uAmp, # quiescent current for both amps + ), + [Power], + ) + self.vn = self.Port(Ground(), [Common]) - analog_in_model = AnalogSink.from_supply( - self.vn, self.vp, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # input common mode absolute maximum ratings - signal_limit_tolerance=(-0.1, 0.1)*Volt, - impedance=6.66e9*Ohm(tol=0), # guess from input bias current - ) - analog_out_model = AnalogSource.from_supply( - self.vn, self.vp, - signal_out_bound=(70*mVolt, -70*mVolt), # output swing from rail, assumed at 10k load - current_limits=(-5, 5)*mAmp, # short circuit current - impedance=2*kOhm(tol=0) # open loop output impedance - ) - self.inpa = self.Port(analog_in_model) - self.inna = self.Port(analog_in_model) - self.outa = self.Port(analog_out_model) - self.inpb = self.Port(analog_in_model) - self.innb = self.Port(analog_in_model) - self.outb = self.Port(analog_out_model) + analog_in_model = AnalogSink.from_supply( + self.vn, + self.vp, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, # input common mode absolute maximum ratings + signal_limit_tolerance=(-0.1, 0.1) * Volt, + impedance=6.66e9 * Ohm(tol=0), # guess from input bias current + ) + analog_out_model = AnalogSource.from_supply( + self.vn, + self.vp, + signal_out_bound=(70 * mVolt, -70 * mVolt), # output swing from rail, assumed at 10k load + current_limits=(-5, 5) * mAmp, # short circuit current + impedance=2 * kOhm(tol=0), # open loop output impedance + ) + self.inpa = self.Port(analog_in_model) + self.inna = self.Port(analog_in_model) + self.outa = self.Port(analog_out_model) + self.inpb = self.Port(analog_in_model) + self.innb = self.Port(analog_in_model) + self.outb = self.Port(analog_out_model) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.outa, - '2': self.inna, - '3': self.inpa, - '4': self.vn, - '5': self.inpb, - '6': self.innb, - '7': self.outb, - '8': self.vp, - }, - mfr='Texas Instruments', part='OPA2333AIDR', - datasheet='https://www.ti.com/lit/ds/symlink/opa2333.pdf' - ) - self.assign(self.lcsc_part, 'C38732') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.outa, + "2": self.inna, + "3": self.inpa, + "4": self.vn, + "5": self.inpb, + "6": self.innb, + "7": self.outb, + "8": self.vp, + }, + mfr="Texas Instruments", + part="OPA2333AIDR", + datasheet="https://www.ti.com/lit/ds/symlink/opa2333.pdf", + ) + self.assign(self.lcsc_part, "C38732") + self.assign(self.actual_basic_part, False) class Opa2333(MultipackOpampGenerator): - """Dual precision RRIO (including negative input) opamps. - """ - @override - def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: - self.ic = self.Block(Opa2333_Device()) - # Datasheet section 9: recommend 0.1uF bypass capacitors close to power supply pins - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.ic.vn, self.ic.vp) + """Dual precision RRIO (including negative input) opamps.""" - return self.OpampPorts(self.ic.vn, self.ic.vp, [ - (self.ic.inna, self.ic.inpa, self.ic.outa), - (self.ic.innb, self.ic.inpb, self.ic.outb), - ]) + @override + def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: + self.ic = self.Block(Opa2333_Device()) + # Datasheet section 9: recommend 0.1uF bypass capacitors close to power supply pins + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.ic.vn, self.ic.vp) + + return self.OpampPorts( + self.ic.vn, + self.ic.vp, + [ + (self.ic.inna, self.ic.inpa, self.ic.outa), + (self.ic.innb, self.ic.inpb, self.ic.outb), + ], + ) diff --git a/edg/parts/Opamp_Tlv9061.py b/edg/parts/Opamp_Tlv9061.py index 3077416ab..80adc046f 100644 --- a/edg/parts/Opamp_Tlv9061.py +++ b/edg/parts/Opamp_Tlv9061.py @@ -5,62 +5,70 @@ class Tlv9061_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vcc = self.Port(VoltageSink( - voltage_limits=(1.8, 5.5)*Volt, current_draw=(538, 800)*uAmp # quiescent current - ), [Power]) - self.vss = self.Port(Ground(), [Common]) + def __init__(self) -> None: + super().__init__() + self.vcc = self.Port( + VoltageSink(voltage_limits=(1.8, 5.5) * Volt, current_draw=(538, 800) * uAmp), [Power] # quiescent current + ) + self.vss = self.Port(Ground(), [Common]) - analog_in_model = AnalogSink.from_supply( - self.vss, self.vcc, - voltage_limit_tolerance=(-0.5, 0.5)*Volt, # common mode maximum ratings - signal_limit_tolerance=(-0.1, 0.1)*Volt - ) - self.vinp = self.Port(analog_in_model) - self.vinn = self.Port(analog_in_model) - self.vout = self.Port(AnalogSource.from_supply( - self.vss, self.vcc, - signal_out_bound=(0, 0), # output voltage range V- to V+ - current_limits=(-50, 50)*mAmp, # for Vs=5V - impedance=100*Ohm(tol=0) # no tolerance bounds given on datasheet; open-loop impedance - )) + analog_in_model = AnalogSink.from_supply( + self.vss, + self.vcc, + voltage_limit_tolerance=(-0.5, 0.5) * Volt, # common mode maximum ratings + signal_limit_tolerance=(-0.1, 0.1) * Volt, + ) + self.vinp = self.Port(analog_in_model) + self.vinn = self.Port(analog_in_model) + self.vout = self.Port( + AnalogSource.from_supply( + self.vss, + self.vcc, + signal_out_bound=(0, 0), # output voltage range V- to V+ + current_limits=(-50, 50) * mAmp, # for Vs=5V + impedance=100 * Ohm(tol=0), # no tolerance bounds given on datasheet; open-loop impedance + ) + ) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', - { - '1': self.vout, - '2': self.vss, - '3': self.vinp, - '4': self.vinn, - '5': self.vcc, # SHDN, active low (pull high to enable) - '6': self.vcc, - }, - mfr='Texas Instruments', part='TLV9061S', - datasheet='https://www.ti.com/lit/ds/symlink/tlv9062-q1.pdf' - ) - self.assign(self.lcsc_part, 'C2058350') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-6", + { + "1": self.vout, + "2": self.vss, + "3": self.vinp, + "4": self.vinn, + "5": self.vcc, # SHDN, active low (pull high to enable) + "6": self.vcc, + }, + mfr="Texas Instruments", + part="TLV9061S", + datasheet="https://www.ti.com/lit/ds/symlink/tlv9062-q1.pdf", + ) + self.assign(self.lcsc_part, "C2058350") + self.assign(self.actual_basic_part, False) class Tlv9061(Opamp): - """RRIO op-amp in SOT-23-6. - """ - @override - def contents(self) -> None: - super().contents() + """RRIO op-amp in SOT-23-6.""" - self.ic = self.Block(Tlv9061_Device()) - self.connect(self.inn, self.ic.vinn) - self.connect(self.inp, self.ic.vinp) - self.connect(self.out, self.ic.vout) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.vss) + @override + def contents(self) -> None: + super().contents() - # Datasheet section 11: place 0.1uF bypass capacitors close to the power-supply pins - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + self.ic = self.Block(Tlv9061_Device()) + self.connect(self.inn, self.ic.vinn) + self.connect(self.inp, self.ic.vinp) + self.connect(self.out, self.ic.vout) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.vss) + + # Datasheet section 11: place 0.1uF bypass capacitors close to the power-supply pins + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) diff --git a/edg/parts/Opamp_Tlv915x.py b/edg/parts/Opamp_Tlv915x.py index e1449eb82..7706932d8 100644 --- a/edg/parts/Opamp_Tlv915x.py +++ b/edg/parts/Opamp_Tlv915x.py @@ -5,66 +5,80 @@ class Tlv9152_Device(InternalSubcircuit, JlcPart, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vp = self.Port(VoltageSink( - voltage_limits=(2.7, 16)*Volt, current_draw=(560*2, 750*2)*uAmp # quiescent current for both amps - ), [Power]) - self.vn = self.Port(Ground(), [Common]) + def __init__(self) -> None: + super().__init__() + self.vp = self.Port( + VoltageSink( + voltage_limits=(2.7, 16) * Volt, + current_draw=(560 * 2, 750 * 2) * uAmp, # quiescent current for both amps + ), + [Power], + ) + self.vn = self.Port(Ground(), [Common]) - analog_in_model = AnalogSink.from_supply( - self.vn, self.vp, - voltage_limit_tolerance=(-0.5, 0.5)*Volt, # input common mode absolute maximum ratings - signal_limit_tolerance=(-0.1, 0.1)*Volt, # common-mode voltage - impedance=100*MOhm(tol=0), # differential input impedance - ) - analog_out_model = AnalogSource.from_supply( - self.vn, self.vp, - signal_out_bound=(55*mVolt, -55*mVolt), # output swing from rail, assumed at 10k load, Vs=16v - current_limits=(-75, 75)*mAmp, # short circuit current - impedance=525*kOhm(tol=0) # open loop output impedance - ) - self.inpa = self.Port(analog_in_model) - self.inna = self.Port(analog_in_model) - self.outa = self.Port(analog_out_model) - self.inpb = self.Port(analog_in_model) - self.innb = self.Port(analog_in_model) - self.outb = self.Port(analog_out_model) + analog_in_model = AnalogSink.from_supply( + self.vn, + self.vp, + voltage_limit_tolerance=(-0.5, 0.5) * Volt, # input common mode absolute maximum ratings + signal_limit_tolerance=(-0.1, 0.1) * Volt, # common-mode voltage + impedance=100 * MOhm(tol=0), # differential input impedance + ) + analog_out_model = AnalogSource.from_supply( + self.vn, + self.vp, + signal_out_bound=(55 * mVolt, -55 * mVolt), # output swing from rail, assumed at 10k load, Vs=16v + current_limits=(-75, 75) * mAmp, # short circuit current + impedance=525 * kOhm(tol=0), # open loop output impedance + ) + self.inpa = self.Port(analog_in_model) + self.inna = self.Port(analog_in_model) + self.outa = self.Port(analog_out_model) + self.inpb = self.Port(analog_in_model) + self.innb = self.Port(analog_in_model) + self.outb = self.Port(analog_out_model) - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', - { - '1': self.outa, - '2': self.inna, - '3': self.inpa, - '4': self.vn, - '5': self.inpb, - '6': self.innb, - '7': self.outb, - '8': self.vp, - }, - mfr='Texas Instruments', part='TLV9152IDR', - datasheet='https://www.ti.com/lit/ds/symlink/tlv9152.pdf' - ) - self.assign(self.lcsc_part, 'C882649') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + { + "1": self.outa, + "2": self.inna, + "3": self.inpa, + "4": self.vn, + "5": self.inpb, + "6": self.innb, + "7": self.outb, + "8": self.vp, + }, + mfr="Texas Instruments", + part="TLV9152IDR", + datasheet="https://www.ti.com/lit/ds/symlink/tlv9152.pdf", + ) + self.assign(self.lcsc_part, "C882649") + self.assign(self.actual_basic_part, False) class Tlv9152(MultipackOpampGenerator): - """Dual RRIO opamps. - """ - @override - def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: - self.ic = self.Block(Tlv9152_Device()) - # Datasheet section 9: recommend 0.1uF bypass capacitors close to power supply pins - self.vdd_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(self.ic.vn, self.ic.vp) + """Dual RRIO opamps.""" - return self.OpampPorts(self.ic.vn, self.ic.vp, [ - (self.ic.inna, self.ic.inpa, self.ic.outa), - (self.ic.innb, self.ic.inpb, self.ic.outb), - ]) + @override + def _make_multipack_opamp(self) -> MultipackOpampGenerator.OpampPorts: + self.ic = self.Block(Tlv9152_Device()) + # Datasheet section 9: recommend 0.1uF bypass capacitors close to power supply pins + self.vdd_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(self.ic.vn, self.ic.vp) + + return self.OpampPorts( + self.ic.vn, + self.ic.vp, + [ + (self.ic.inna, self.ic.inpa, self.ic.outa), + (self.ic.innb, self.ic.inpb, self.ic.outb), + ], + ) diff --git a/edg/parts/PassiveConnector_Fpc.py b/edg/parts/PassiveConnector_Fpc.py index 5528534e9..518f885e6 100644 --- a/edg/parts/PassiveConnector_Fpc.py +++ b/edg/parts/PassiveConnector_Fpc.py @@ -8,190 +8,279 @@ @abstract_block class Fpc050(PassiveConnector): - """Abstract base class for 0.50mm pitch FPC connectors.""" + """Abstract base class for 0.50mm pitch FPC connectors.""" @abstract_block class Fpc050Top(Fpc050): - """Abstract base class for top-contact FPC connectors. - IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" + """Abstract base class for top-contact FPC connectors. + IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" @abstract_block class Fpc050Bottom(Fpc050): - """Abstract base class for bottom-contact FPC connectors. - IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" + """Abstract base class for bottom-contact FPC connectors. + IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" class Fpc050BottomFlip(Fpc050Bottom, GeneratorBlock): - """Flipped FPC connector - bottom entry connector is top entry on the opposite board side. - Reverses the pin ordering to reflect the mirroring.""" - @override - def contents(self) -> None: - super().contents() - self.generator_param(self.length, self.pins.requested()) - - @override - def generate(self) -> None: - super().generate() - self.conn = self.Block(Fpc050Top(self.length)) - length = self.get(self.length) - for pin in self.get(self.pins.requested()): - self.connect(self.pins.append_elt(Passive.empty(), pin), - self.conn.pins.request(str(length - (int(pin) - 1)))) + """Flipped FPC connector - bottom entry connector is top entry on the opposite board side. + Reverses the pin ordering to reflect the mirroring.""" + + @override + def contents(self) -> None: + super().contents() + self.generator_param(self.length, self.pins.requested()) + + @override + def generate(self) -> None: + super().generate() + self.conn = self.Block(Fpc050Top(self.length)) + length = self.get(self.length) + for pin in self.get(self.pins.requested()): + self.connect( + self.pins.append_elt(Passive.empty(), pin), self.conn.pins.request(str(length - (int(pin) - 1))) + ) class HiroseFh12sh(Fpc050Bottom, FootprintPassiveConnector): - """Hirose FH12 SH FFC/FPC connector, 0.50mm pitch horizontal bottom contacts. - Mostly footprint-compatible with TE 1775333-8, which is cheaper.""" - # positions the FH12 exists in, per https://www.hirose.com/product/series/FH12 - _fh12_pins = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 24, 25, 26, 28, 29, - 30, 32, 33, 34, 35, 36, 40, 42, 45, 49, 50, 53, 60} - # positions for which there are KiCad footprints - _kicad_pins = {6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 24, 25, 26, 28, - 30, 32, 33, 34, 35, 36, 40, 45, 50, 53} - allowed_pins = _fh12_pins.intersection(_kicad_pins) - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_FFC-FPC:Hirose_FH12-{length}S-0.5SH_1x{length:02d}-1MP_P0.50mm_Horizontal', - "Hirose", f"FH12-{length}S-0.5SH") + """Hirose FH12 SH FFC/FPC connector, 0.50mm pitch horizontal bottom contacts. + Mostly footprint-compatible with TE 1775333-8, which is cheaper.""" + + # positions the FH12 exists in, per https://www.hirose.com/product/series/FH12 + _fh12_pins = { + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 25, + 26, + 28, + 29, + 30, + 32, + 33, + 34, + 35, + 36, + 40, + 42, + 45, + 49, + 50, + 53, + 60, + } + # positions for which there are KiCad footprints + _kicad_pins = { + 6, + 8, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 25, + 26, + 28, + 30, + 32, + 33, + 34, + 35, + 36, + 40, + 45, + 50, + 53, + } + allowed_pins = _fh12_pins.intersection(_kicad_pins) + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_FFC-FPC:Hirose_FH12-{length}S-0.5SH_1x{length:02d}-1MP_P0.50mm_Horizontal", + "Hirose", + f"FH12-{length}S-0.5SH", + ) class Afc01(Fpc050Bottom, FootprintPassiveConnector, JlcPart): - """Jushuo AFC01 series bottom-contact 0.5mm-pitch FPC connectors, with partial JLC numbers for some parts - and re-using the probably-compatible but not-purpose-designed FH12 footprint.""" - _afc01_pins = set(range(4, 60 + 1)) # as listed by the part table - allowed_pins = _afc01_pins.intersection(HiroseFh12sh._kicad_pins) - PART_NUMBERS = { # partial list of the ones currently used - # including -FCC (tube) and -FCA (T&R) suffix - 4: 'C262260', # FCC - 5: 'C262654', # FCA - 6: 'C262262', # FCC - 7: 'C262263', # FCC - 8: 'C262657', # also C262264 for -FCC - 9: 'C262265', # FCC - 10: 'C262266', # FCC - 12: 'C262268', # FCC - 13: 'C262269', # FCC - 14: 'C577443', # FCC - 15: 'C262664', # also C262271 for -FCC - 16: 'C262272', # FCC - 18: 'C262273', # FCC - 20: 'C262274', # FCC - 22: 'C262275', # FCC - 24: 'C262669', # also C262276 for -FCC - 26: 'C262277', # FCC - 28: 'C262278', # FCC - 30: 'C262671', # also C262279 for -FCC - 32: 'C262280', # FCC - 36: 'C262673', # FCA - 40: 'C262674', # also C262282 for FCC - 45: 'C13507', # FCC - 50: 'C262676', # also C262284 for FCC - 54: 'C262677', # FCA - 60: 'C2918970' # FCC - } - - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - # TODO this isn't the intended hook and uses side effects, but it works for now - self.assign(self.lcsc_part, self.PART_NUMBERS[length]) - self.assign(self.actual_basic_part, False) - return (f'Connector_FFC-FPC:Hirose_FH12-{length}S-0.5SH_1x{length:02d}-1MP_P0.50mm_Horizontal', - "Jushuo", f"AFC01-S{length:02d}FC*-00") # CA is T&R packaging + """Jushuo AFC01 series bottom-contact 0.5mm-pitch FPC connectors, with partial JLC numbers for some parts + and re-using the probably-compatible but not-purpose-designed FH12 footprint.""" + + _afc01_pins = set(range(4, 60 + 1)) # as listed by the part table + allowed_pins = _afc01_pins.intersection(HiroseFh12sh._kicad_pins) + PART_NUMBERS = { # partial list of the ones currently used + # including -FCC (tube) and -FCA (T&R) suffix + 4: "C262260", # FCC + 5: "C262654", # FCA + 6: "C262262", # FCC + 7: "C262263", # FCC + 8: "C262657", # also C262264 for -FCC + 9: "C262265", # FCC + 10: "C262266", # FCC + 12: "C262268", # FCC + 13: "C262269", # FCC + 14: "C577443", # FCC + 15: "C262664", # also C262271 for -FCC + 16: "C262272", # FCC + 18: "C262273", # FCC + 20: "C262274", # FCC + 22: "C262275", # FCC + 24: "C262669", # also C262276 for -FCC + 26: "C262277", # FCC + 28: "C262278", # FCC + 30: "C262671", # also C262279 for -FCC + 32: "C262280", # FCC + 36: "C262673", # FCA + 40: "C262674", # also C262282 for FCC + 45: "C13507", # FCC + 50: "C262676", # also C262284 for FCC + 54: "C262677", # FCA + 60: "C2918970", # FCC + } + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + # TODO this isn't the intended hook and uses side effects, but it works for now + self.assign(self.lcsc_part, self.PART_NUMBERS[length]) + self.assign(self.actual_basic_part, False) + return ( + f"Connector_FFC-FPC:Hirose_FH12-{length}S-0.5SH_1x{length:02d}-1MP_P0.50mm_Horizontal", + "Jushuo", + f"AFC01-S{length:02d}FC*-00", + ) # CA is T&R packaging class Afc07Top(Fpc050Top, FootprintPassiveConnector, JlcPart): - """Jushuo AFC07 series slide-lock top-contact 0.5mm-pitch FPC connectors, with partial JLC numbers for some parts - and re-using the probably-compatible but not-purpose-designed FH12 footprint.""" - _afc07_pins = set(range(4, 60 + 1)) # as listed by the part table - allowed_pins = _afc07_pins.intersection(HiroseFh12sh._kicad_pins) - PART_NUMBERS = { # partial list of the ones currently used - # including -ECC (tube) and -ECA (T&R) suffix - 4: 'C2764271', # ECA - 5: 'C262230', # also C262578 for -ECA - 6: 'C413943', # ECC - 7: 'C262232', # ECC - 8: 'C262581', # also C11084 for -ECC - 10: 'C262583', # ECA - 12: 'C11086', # ECC - 13: 'C262238', # ECC - 14: 'C11087', # also C262587 for -ECA - 15: 'C262240', # also C262588 for -ECA - 16: 'C11088', # ECC - 18: 'C11089', # also C2840708 for -ECC - 20: 'C262641', # ECA - 22: 'C262642', # also C11091 for -ECC - 24: 'C262643', # also C11092 for -ECC - 26: 'C262644', # also C11094 for -ECC - 28: 'C2886796', # ECA - 30: 'C262645', # also C262645 for -ECA - 32: 'C11096', # ECC - 34: 'C262252', # ECC - 36: 'C262254', # ECC - 40: 'C262648', # also C11097 for -ECC - 45: 'C262256', # ECC - 50: 'C262650', # also C11098 for -ECC - 54: 'C262258', # also C2691600 for -ECC - 60: 'C262652' # ECA - } - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - # TODO this isn't the intended hook and uses side effects, but it works for now - self.assign(self.lcsc_part, self.PART_NUMBERS[length]) - self.assign(self.actual_basic_part, False) - return (f'Connector_FFC-FPC:TE_{length//10}-1734839-{length%10}_1x{length:02d}-1MP_P0.5mm_Horizontal', - "Jushuo", f"AFC07-S{length:02d}EC*-00") # CA is packaging + """Jushuo AFC07 series slide-lock top-contact 0.5mm-pitch FPC connectors, with partial JLC numbers for some parts + and re-using the probably-compatible but not-purpose-designed FH12 footprint.""" + + _afc07_pins = set(range(4, 60 + 1)) # as listed by the part table + allowed_pins = _afc07_pins.intersection(HiroseFh12sh._kicad_pins) + PART_NUMBERS = { # partial list of the ones currently used + # including -ECC (tube) and -ECA (T&R) suffix + 4: "C2764271", # ECA + 5: "C262230", # also C262578 for -ECA + 6: "C413943", # ECC + 7: "C262232", # ECC + 8: "C262581", # also C11084 for -ECC + 10: "C262583", # ECA + 12: "C11086", # ECC + 13: "C262238", # ECC + 14: "C11087", # also C262587 for -ECA + 15: "C262240", # also C262588 for -ECA + 16: "C11088", # ECC + 18: "C11089", # also C2840708 for -ECC + 20: "C262641", # ECA + 22: "C262642", # also C11091 for -ECC + 24: "C262643", # also C11092 for -ECC + 26: "C262644", # also C11094 for -ECC + 28: "C2886796", # ECA + 30: "C262645", # also C262645 for -ECA + 32: "C11096", # ECC + 34: "C262252", # ECC + 36: "C262254", # ECC + 40: "C262648", # also C11097 for -ECC + 45: "C262256", # ECC + 50: "C262650", # also C11098 for -ECC + 54: "C262258", # also C2691600 for -ECC + 60: "C262652", # ECA + } + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + # TODO this isn't the intended hook and uses side effects, but it works for now + self.assign(self.lcsc_part, self.PART_NUMBERS[length]) + self.assign(self.actual_basic_part, False) + return ( + f"Connector_FFC-FPC:TE_{length//10}-1734839-{length%10}_1x{length:02d}-1MP_P0.5mm_Horizontal", + "Jushuo", + f"AFC07-S{length:02d}EC*-00", + ) # CA is packaging class Te1734839(Fpc050Top, FootprintPassiveConnector): - """TE x-1734839 FFC/FPC connector, 0.50mm pitch horizontal top contacts.""" - allowed_positions = range(5, 50) + """TE x-1734839 FFC/FPC connector, 0.50mm pitch horizontal top contacts.""" + + allowed_positions = range(5, 50) - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_FFC-FPC:TE_{length // 10}-1734839-{length % 10}_1x{length:02d}-1MP_P0.5mm_Horizontal', - "TE Connectivity", f"{length // 10}-1734839-{length % 10}") + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_FFC-FPC:TE_{length // 10}-1734839-{length % 10}_1x{length:02d}-1MP_P0.5mm_Horizontal", + "TE Connectivity", + f"{length // 10}-1734839-{length % 10}", + ) @abstract_block class Fpc030(PassiveConnector): - """Abstract base class for 0.30mm pitch (dual row, staggered)) FPC connectors.""" + """Abstract base class for 0.30mm pitch (dual row, staggered)) FPC connectors.""" @abstract_block class Fpc030Top(Fpc030): - """Abstract base class for top-contact FPC connectors. - IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" + """Abstract base class for top-contact FPC connectors. + IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" @abstract_block class Fpc030Bottom(Fpc030): - """Abstract base class for bottom-contact FPC connectors. - IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" + """Abstract base class for bottom-contact FPC connectors. + IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" @abstract_block class Fpc030TopBottom(Fpc030Bottom): - """Abstract base class for top and bottom-contact FPC connectors. Bottom entry pin numbering is treated as canonical. - To use in place of a top-contact connector, a flip is needed. - IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" + """Abstract base class for top and bottom-contact FPC connectors. Bottom entry pin numbering is treated as canonical. + To use in place of a top-contact connector, a flip is needed. + IMPORTANT: the pin numbering scheme differs for top- and bottom-contact connectors.""" class HiroseFh35cshw(Fpc030TopBottom, FootprintPassiveConnector, JlcPart): - """Hirose FH35C SHW FFC/FPC connector, 0.30mm pitch horizontal top/bottom contacts.""" - # positions the FH35C exists in, per datasheet - _fh35c_pins = {9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 31, 33, 35, 37, 39, 41, 45, 49, 51, 55, 61} - # positions for which there are KiCad footprints - _kicad_pins = {31} - allowed_pins = _fh35c_pins.intersection(_kicad_pins) - PART_NUMBERS = { # partial list of the ones currently used - 31: 'C424662', - } - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - # TODO this isn't the intended hook and uses side effects, but it works for now - self.assign(self.lcsc_part, self.PART_NUMBERS[length]) - self.assign(self.actual_basic_part, False) - return (f'edg:Hirose_FH35C-{length}S-0.3SHW_1x{length:02d}-1MP_P0.30mm_Horizontal', - "Hirose", f"FH35C-{length}S-0.3SHW") + """Hirose FH35C SHW FFC/FPC connector, 0.30mm pitch horizontal top/bottom contacts.""" + + # positions the FH35C exists in, per datasheet + _fh35c_pins = {9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 31, 33, 35, 37, 39, 41, 45, 49, 51, 55, 61} + # positions for which there are KiCad footprints + _kicad_pins = {31} + allowed_pins = _fh35c_pins.intersection(_kicad_pins) + PART_NUMBERS = { # partial list of the ones currently used + 31: "C424662", + } + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + # TODO this isn't the intended hook and uses side effects, but it works for now + self.assign(self.lcsc_part, self.PART_NUMBERS[length]) + self.assign(self.actual_basic_part, False) + return ( + f"edg:Hirose_FH35C-{length}S-0.3SHW_1x{length:02d}-1MP_P0.30mm_Horizontal", + "Hirose", + f"FH35C-{length}S-0.3SHW", + ) diff --git a/edg/parts/PassiveConnector_Header.py b/edg/parts/PassiveConnector_Header.py index 14492baa0..76081edbf 100644 --- a/edg/parts/PassiveConnector_Header.py +++ b/edg/parts/PassiveConnector_Header.py @@ -8,210 +8,267 @@ @abstract_block_default(lambda: PinHeader254Vertical) class PinHeader254(PassiveConnector): - """Abstract base class for all 2.54mm pin headers.""" + """Abstract base class for all 2.54mm pin headers.""" class PinHeader254Vertical(PinHeader254, FootprintPassiveConnector): - """Generic 2.54mm pin header in vertical through-hole.""" - allowed_pins = range(1, 40+1) - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_PinHeader_2.54mm:PinHeader_1x{length:02d}_P2.54mm_Vertical', - "Generic", f"PinHeader2.54 1x{length}") + """Generic 2.54mm pin header in vertical through-hole.""" + + allowed_pins = range(1, 40 + 1) + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_PinHeader_2.54mm:PinHeader_1x{length:02d}_P2.54mm_Vertical", + "Generic", + f"PinHeader2.54 1x{length}", + ) class PinHeader254Horizontal(PinHeader254, FootprintPassiveConnector): - """Generic 2.54mm pin header in horizontal (right-angle) through-hole.""" - allowed_pins = range(1, 40+1) - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_PinHeader_2.54mm:PinHeader_1x{length:02d}_P2.54mm_Horizontal', - "Generic", f"PinHeader2.54 1x{length} Horizontal") + """Generic 2.54mm pin header in horizontal (right-angle) through-hole.""" + + allowed_pins = range(1, 40 + 1) + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_PinHeader_2.54mm:PinHeader_1x{length:02d}_P2.54mm_Horizontal", + "Generic", + f"PinHeader2.54 1x{length} Horizontal", + ) class PinSocket254(FootprintPassiveConnector): - """Generic 2.54mm pin socket in vertical through-hole.""" - allowed_pins = range(1, 40+1) - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_PinSocket_2.54mm:PinSocket_1x{length:02d}_P2.54mm_Vertical', - "Generic", f"PinSocket2.54 1x{length}") + """Generic 2.54mm pin socket in vertical through-hole.""" + + allowed_pins = range(1, 40 + 1) + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_PinSocket_2.54mm:PinSocket_1x{length:02d}_P2.54mm_Vertical", + "Generic", + f"PinSocket2.54 1x{length}", + ) class PinHeader254DualShroudedInline(FootprintPassiveConnector): - """Generic 2.54mm dual-row pin header in edge-inline.""" - allowed_pins = {6} - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'edg:PinHeader_2x{length//2:02d}_P2.54mm_EdgeInline', - "Generic", f"PinHeader2.54 Shrouded 2x{length//2}") + """Generic 2.54mm dual-row pin header in edge-inline.""" + + allowed_pins = {6} + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"edg:PinHeader_2x{length//2:02d}_P2.54mm_EdgeInline", + "Generic", + f"PinHeader2.54 Shrouded 2x{length//2}", + ) class PinHeader127DualShrouded(FootprintPassiveConnector, JlcPart): - """Generic dual-row 1.27mm pin header in vertical through-hole pinned in zigzag.""" - allowed_pins = [10] # TODO support more - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - assert length == 10, "TODO support more lengths" - self.assign(self.lcsc_part, 'C2962219') - self.assign(self.actual_basic_part, False) - # TODO shrouded footprint - return (f'Connector_PinHeader_1.27mm:PinHeader_2x{length//2:02d}_P1.27mm_Vertical_SMD', - "Generic", f"PinHeader1.27 Shrouded 2x{length//2}") + """Generic dual-row 1.27mm pin header in vertical through-hole pinned in zigzag.""" + + allowed_pins = [10] # TODO support more + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + assert length == 10, "TODO support more lengths" + self.assign(self.lcsc_part, "C2962219") + self.assign(self.actual_basic_part, False) + # TODO shrouded footprint + return ( + f"Connector_PinHeader_1.27mm:PinHeader_2x{length//2:02d}_P1.27mm_Vertical_SMD", + "Generic", + f"PinHeader1.27 Shrouded 2x{length//2}", + ) @abstract_block_default(lambda: JstXhAVertical) class JstXh(FootprintPassiveConnector): - """Abstract base class for JST XH 2.50mm shrouded and polarized headers.""" + """Abstract base class for JST XH 2.50mm shrouded and polarized headers.""" class JstXhAVertical(JstXh): - """JST B*B-XH-A series connector: 2.50mm shrouded and polarized, in vertical through-hole.""" - allowed_pins = list(range(2, 16+1)) + [20] - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_JST:JST_XH_B{length}B-XH-A_1x{length:02d}_P2.50mm_Vertical', - "JST", f"B{length}B-XH-A") + """JST B*B-XH-A series connector: 2.50mm shrouded and polarized, in vertical through-hole.""" + + allowed_pins = list(range(2, 16 + 1)) + [20] + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return (f"Connector_JST:JST_XH_B{length}B-XH-A_1x{length:02d}_P2.50mm_Vertical", "JST", f"B{length}B-XH-A") class JstXhAHorizontal(JstXh): - """JST S*B-XH-A series connector: 2.50mm shrouded and polarized, in horizontal through-hole.""" - allowed_pins = list(range(2, 16+1)) + [20] - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_JST:JST_XH_S{length}B-XH-A_1x{length:02d}_P2.50mm_Vertical', - "JST", f"S{length}B-XH-A") + """JST S*B-XH-A series connector: 2.50mm shrouded and polarized, in horizontal through-hole.""" + + allowed_pins = list(range(2, 16 + 1)) + [20] + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return (f"Connector_JST:JST_XH_S{length}B-XH-A_1x{length:02d}_P2.50mm_Vertical", "JST", f"S{length}B-XH-A") @abstract_block_default(lambda: JstPhKVertical) class JstPh(FootprintPassiveConnector): - """Abstract base class for JST PH 2.00mm shrouded and polarized headers.""" + """Abstract base class for JST PH 2.00mm shrouded and polarized headers.""" class JstPhKVertical(JstPh): - """JST B*B-PH-K series connector: 2.00mm shrouded and polarized, in vertical through-hole.""" - allowed_pins = range(2, 16+1) - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_JST:JST_PH_B{length}B-PH-K_1x{length:02d}_P2.00mm_Vertical', - "JST", f"B{length}B-PH-K") + """JST B*B-PH-K series connector: 2.00mm shrouded and polarized, in vertical through-hole.""" + + allowed_pins = range(2, 16 + 1) + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return (f"Connector_JST:JST_PH_B{length}B-PH-K_1x{length:02d}_P2.00mm_Vertical", "JST", f"B{length}B-PH-K") """JST S*B-PH-K series connector: 2.00mm shrouded and polarized, in horizontal (right-angle) through-hole.""" + + class JstPhKHorizontal(JstPh, JlcPart): - allowed_pins = range(2, 16+1) - PART_NUMBERS = { # white colored, -S part suffix - 2: 'C173752', - 3: 'C157929', - 4: 'C157926', - 5: 'C157923', - 6: 'C157920', - 7: 'C157917', - 8: 'C157915', - 9: 'C157912', - 10: 'C157947', - 11: 'C157945', - 12: 'C157943', - 13: 'C157940', - 14: 'C157938', - 15: 'C157936', - 16: 'C157934', - } - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - self.assign(self.lcsc_part, self.PART_NUMBERS[length]) - self.assign(self.actual_basic_part, False) - return (f'Connector_JST:JST_PH_S{length}B-PH-K_1x{length:02d}_P2.00mm_Horizontal', - "JST", f"S{length}B-PH-K") + allowed_pins = range(2, 16 + 1) + PART_NUMBERS = { # white colored, -S part suffix + 2: "C173752", + 3: "C157929", + 4: "C157926", + 5: "C157923", + 6: "C157920", + 7: "C157917", + 8: "C157915", + 9: "C157912", + 10: "C157947", + 11: "C157945", + 12: "C157943", + 13: "C157940", + 14: "C157938", + 15: "C157936", + 16: "C157934", + } + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + self.assign(self.lcsc_part, self.PART_NUMBERS[length]) + self.assign(self.actual_basic_part, False) + return (f"Connector_JST:JST_PH_S{length}B-PH-K_1x{length:02d}_P2.00mm_Horizontal", "JST", f"S{length}B-PH-K") class JstPhSmVertical(JstPh): - """JST B*B-PH-SM4 series connector: 2.00mm shrouded and polarized, in vertical surface-mount.""" - allowed_pins = range(2, 16+1) - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_JST:JST_PH_B{length}B-PH-SM4-TB_1x{length:02d}-1MP_P2.00mm_Vertical', - "JST", f"B{length}B-PH-SM4-TB") + """JST B*B-PH-SM4 series connector: 2.00mm shrouded and polarized, in vertical surface-mount.""" + + allowed_pins = range(2, 16 + 1) + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_JST:JST_PH_B{length}B-PH-SM4-TB_1x{length:02d}-1MP_P2.00mm_Vertical", + "JST", + f"B{length}B-PH-SM4-TB", + ) class JstPhSmVerticalJlc(JlcPart, JstPhSmVertical): - """JST PH connector in SMD, with JLC part numbers for what parts are stocked (JST or clones, - since JLC's inventory of PH SMD connectors is pretty spotty).""" - PART_NUMBERS = { # in order of decreasing stock, on 2022-08-23 - 3: 'C146054', - 2: 'C64658', - 6: 'C265088', - 5: 'C273126', - 4: 'C519161', - 8: 'C519165', - 14: 'C278813', - } - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - # TODO this isn't the intended hook and uses side effects, but it works for now - self.assign(self.lcsc_part, self.PART_NUMBERS[length]) - self.assign(self.actual_basic_part, False) - return super().part_footprint_mfr_name(length) + """JST PH connector in SMD, with JLC part numbers for what parts are stocked (JST or clones, + since JLC's inventory of PH SMD connectors is pretty spotty).""" + + PART_NUMBERS = { # in order of decreasing stock, on 2022-08-23 + 3: "C146054", + 2: "C64658", + 6: "C265088", + 5: "C273126", + 4: "C519161", + 8: "C519165", + 14: "C278813", + } + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + # TODO this isn't the intended hook and uses side effects, but it works for now + self.assign(self.lcsc_part, self.PART_NUMBERS[length]) + self.assign(self.actual_basic_part, False) + return super().part_footprint_mfr_name(length) class JstShSmHorizontal(FootprintPassiveConnector, JlcPart): - """JST SH connector in SMD, with JLC part numbers for what parts are stocked.""" - PART_NUMBERS = { # in order of decreasing stock, on 2022-08-23 - 2: 'C160402', - 3: 'C160403', - 4: 'C160404', - 5: 'C136657', - 6: 'C160405', - 7: 'C160406', - 8: 'C160407', - 9: 'C160408', - 10: 'C160409', - 11: 'C515956', - 12: 'C160411', - 13: 'C160412', - 14: 'C160413', - 15: 'C160414', - 20: 'C160415', - } - allowed_pins = PART_NUMBERS.keys() - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - # TODO this isn't the intended hook and uses side effects, but it works for now - self.assign(self.lcsc_part, self.PART_NUMBERS[length]) - self.assign(self.actual_basic_part, False) - return (f'Connector_JST:JST_SH_SM{length:02d}B-SRSS-TB_1x{length:02d}-1MP_P1.00mm_Horizontal', - "JST", f"SM{length:02d}B-SRSS-TB") + """JST SH connector in SMD, with JLC part numbers for what parts are stocked.""" + + PART_NUMBERS = { # in order of decreasing stock, on 2022-08-23 + 2: "C160402", + 3: "C160403", + 4: "C160404", + 5: "C136657", + 6: "C160405", + 7: "C160406", + 8: "C160407", + 9: "C160408", + 10: "C160409", + 11: "C515956", + 12: "C160411", + 13: "C160412", + 14: "C160413", + 15: "C160414", + 20: "C160415", + } + allowed_pins = PART_NUMBERS.keys() + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + # TODO this isn't the intended hook and uses side effects, but it works for now + self.assign(self.lcsc_part, self.PART_NUMBERS[length]) + self.assign(self.actual_basic_part, False) + return ( + f"Connector_JST:JST_SH_SM{length:02d}B-SRSS-TB_1x{length:02d}-1MP_P1.00mm_Horizontal", + "JST", + f"SM{length:02d}B-SRSS-TB", + ) @abstract_block_default(lambda: Picoblade53398) class Picoblade(FootprintPassiveConnector): - """Abstract base class for Molex PicoBlade 1.25mm shrouded and polarized headers. - Sometimes generically referred to as JST 1.25mm, even though JST does not make 1.25mm headers.""" + """Abstract base class for Molex PicoBlade 1.25mm shrouded and polarized headers. + Sometimes generically referred to as JST 1.25mm, even though JST does not make 1.25mm headers.""" class Picoblade53398(Picoblade): - """Picoblade connector in surface-mount vertical.""" - allowed_pins = list(range(2, 16+1)) + [20] - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_Molex:Molex_PicoBlade_53398-{length:02d}71_1x{length:02d}-1MP_P1.25mm_Vertical', - "Molex", f"53398{length:02d}71") + """Picoblade connector in surface-mount vertical.""" + + allowed_pins = list(range(2, 16 + 1)) + [20] + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_Molex:Molex_PicoBlade_53398-{length:02d}71_1x{length:02d}-1MP_P1.25mm_Vertical", + "Molex", + f"53398{length:02d}71", + ) class Picoblade53261(Picoblade): - """Picoblade connector in surface-mount horizontal.""" - allowed_pins = list(range(2, 16+1)) + [20] - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_Molex:Molex_PicoBlade_53261-{length:02d}71_1x{length:02d}-1MP_P1.25mm_Vertical', - "Molex", f"53261{length:02d}71") + """Picoblade connector in surface-mount horizontal.""" + + allowed_pins = list(range(2, 16 + 1)) + [20] + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_Molex:Molex_PicoBlade_53261-{length:02d}71_1x{length:02d}-1MP_P1.25mm_Vertical", + "Molex", + f"53261{length:02d}71", + ) class MolexSl(FootprintPassiveConnector): - """Molex SL series connector: 2.54mm shrouded and polarized, in vertical through-hole. - Breadboard wire compatible - especially for debugging in a pinch.""" - allowed_pins = range(2, 25+1) - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector_Molex:Molex_SL_171971-00{length:02d}_1x{length:02d}_P2.54mm_Vertical', - "Molex", f"171971-00{length:02d}_1x{length:02d}") + """Molex SL series connector: 2.54mm shrouded and polarized, in vertical through-hole. + Breadboard wire compatible - especially for debugging in a pinch.""" + + allowed_pins = range(2, 25 + 1) + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector_Molex:Molex_SL_171971-00{length:02d}_1x{length:02d}_P2.54mm_Vertical", + "Molex", + f"171971-00{length:02d}_1x{length:02d}", + ) diff --git a/edg/parts/PassiveConnector_TagConnect.py b/edg/parts/PassiveConnector_TagConnect.py index 5c70aca47..f239274e4 100644 --- a/edg/parts/PassiveConnector_TagConnect.py +++ b/edg/parts/PassiveConnector_TagConnect.py @@ -7,20 +7,32 @@ @abstract_block_default(lambda: TagConnectLegged) class TagConnect(FootprintPassiveConnector): - """Abstract block for tag-connect pogo pin pads.""" + """Abstract block for tag-connect pogo pin pads.""" class TagConnectLegged(TagConnect): - """Tag-connect pogo pin pad for the legged version. Compatible with non-legged versions.""" - allowed_pins = {6, 10, 14} # KiCad only has footprints for 2x03, 2x05, 2x07 - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector:Tag-Connect_TC20{length // 2}0-IDC-FP_2x{length // 2:02d}_P1.27mm_Vertical', '', '') # no physical part + """Tag-connect pogo pin pad for the legged version. Compatible with non-legged versions.""" + + allowed_pins = {6, 10, 14} # KiCad only has footprints for 2x03, 2x05, 2x07 + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector:Tag-Connect_TC20{length // 2}0-IDC-FP_2x{length // 2:02d}_P1.27mm_Vertical", + "", + "", + ) # no physical part class TagConnectNonLegged(TagConnect): - """Tag-connect pogo pin pad for the non-legged version. NOT compatible with legged versions.""" - allowed_pins = {6, 10} # KiCad only has footprints for 2x03 and 2x05 - @override - def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: - return (f'Connector:Tag-Connect_TC20{length // 2}0-IDC-NL_2x{length // 2:02d}_P1.27mm_Vertical', '', '') # no physical part + """Tag-connect pogo pin pad for the non-legged version. NOT compatible with legged versions.""" + + allowed_pins = {6, 10} # KiCad only has footprints for 2x03 and 2x05 + + @override + def part_footprint_mfr_name(self, length: int) -> Tuple[str, str, str]: + return ( + f"Connector:Tag-Connect_TC20{length // 2}0-IDC-NL_2x{length // 2:02d}_P1.27mm_Vertical", + "", + "", + ) # no physical part diff --git a/edg/parts/PowerConditioning.py b/edg/parts/PowerConditioning.py index d77e3ef62..4e9fc506e 100644 --- a/edg/parts/PowerConditioning.py +++ b/edg/parts/PowerConditioning.py @@ -7,466 +7,559 @@ class Supercap(DiscreteComponent, FootprintBlock): # TODO actually model supercaps and parts selection - def __init__(self) -> None: - super().__init__() - self.pos = self.Port(VoltageSink()) - self.neg = self.Port(Ground()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'C', 'Capacitor_THT:CP_Radial_D14.0mm_P5.00mm', # actually 13.5 - { - '1': self.pos, - '2': self.neg, - }, - part='DBN-5R5D334T', # TODO this is too high resistance - datasheet='http://www.elna.co.jp/en/capacitor/double_layer/catalog/pdf/dbn_e.pdf', - ) + def __init__(self) -> None: + super().__init__() + self.pos = self.Port(VoltageSink()) + self.neg = self.Port(Ground()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "C", + "Capacitor_THT:CP_Radial_D14.0mm_P5.00mm", # actually 13.5 + { + "1": self.pos, + "2": self.neg, + }, + part="DBN-5R5D334T", # TODO this is too high resistance + datasheet="http://www.elna.co.jp/en/capacitor/double_layer/catalog/pdf/dbn_e.pdf", + ) class BufferedSupply(PowerConditioner): - """Implements a current limiting source with an opamp for charging a supercap, and a Vf-limited diode - for discharging - - See https://electronics.stackexchange.com/questions/178605/op-amp-mosfet-constant-current-power-source - """ - def __init__(self, charging_current: RangeLike, sense_resistance: RangeLike, - voltage_drop: RangeLike) -> None: - super().__init__() - - self.charging_current = self.ArgParameter(charging_current) - self.sense_resistance = self.ArgParameter(sense_resistance) - self.voltage_drop = self.ArgParameter(voltage_drop) - - self.pwr = self.Port(VoltageSink.empty(), [Power, Input]) - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) - self.require(self.pwr.current_draw.within(self.pwr_out.link().current_drawn + - (0, self.charging_current.upper()) + - (0, 0.05))) # TODO nonhacky bounds on opamp/sense resistor current draw - self.sc_out = self.Port(VoltageSource.empty(), optional=True) - self.gnd = self.Port(Ground.empty(), [Common]) - - max_in_voltage = self.pwr.link().voltage.upper() - max_charge_current = self.charging_current.upper() - - # Upstream power domain - # TODO improve connect modeling everywhere - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.sense = self.Block(Resistor( # TODO replace with SeriesResistor/CurrentSenseResistor - that propagates current - resistance=self.sense_resistance, - power=(0, max_charge_current * max_charge_current * self.sense_resistance.upper()) - )) - self.connect(self.pwr, self.sense.a.adapt_to(VoltageSink( - current_draw=(0, max_charge_current) - ))) - - self.fet = self.Block(Fet.PFet( - drain_voltage=(0, max_in_voltage), drain_current=(0, max_charge_current), - gate_voltage=(self.pwr.link().voltage.lower(), max_in_voltage), - rds_on=(0, 0.5)*Ohm, # TODO kind of arbitrary - power=(0, max_in_voltage * max_charge_current) - )) - self.connect(self.fet.source, self.sense.b) - - self.diode = self.Block(Diode( - reverse_voltage=(0, max_in_voltage), current=self.charging_current, voltage_drop=self.voltage_drop, - )) - self.connect(self.diode.anode.adapt_to(VoltageSink()), - self.fet.drain.adapt_to(VoltageSource( - voltage_out=self.pwr.link().voltage) - ), - self.sc_out) - - self.pwr_out_merge = self.Block(MergedVoltageSource()).connected_from( - self.pwr, - self.diode.cathode.adapt_to(VoltageSource( - voltage_out=(self.pwr.link().voltage.lower() - self.voltage_drop.upper(), self.pwr.link().voltage.upper()) - )) # TODO replace with SeriesVoltageDiode or something that automatically calculates voltage drops? - ) - self.connect(self.pwr_out_merge.pwr_out, self.pwr_out) - - # TODO check if this tolerance stackup is stacking in the right direction... it might not - low_sense_volt_diff = self.charging_current.lower() * self.sense_resistance.lower() - high_sense_volt_diff = self.charging_current.upper() * self.sense_resistance.upper() - low_sense_volt = self.pwr.link().voltage.lower() - high_sense_volt_diff - high_sense_volt = self.pwr.link().voltage.upper() - low_sense_volt_diff - - self.set = imp.Block(VoltageDivider(output_voltage=(low_sense_volt, high_sense_volt), impedance=(1, 10) * kOhm)) - self.connect(self.set.input, self.pwr) # TODO use chain - self.amp = imp.Block(Opamp()) - self.connect(self.set.output, self.amp.inp) - self.connect(self.amp.inn, self.sense.b.adapt_to(AnalogSource( - voltage_out=(0, self.pwr.link().voltage.upper()), - # TODO calculate operating signal level - ))) - self.connect(self.amp.out, self.fet.gate.adapt_to(AnalogSink())) - - self.cap = self.Block(Supercap()) - self.connect(self.sc_out, self.cap.pos) - self.connect(self.gnd, self.cap.neg) + """Implements a current limiting source with an opamp for charging a supercap, and a Vf-limited diode + for discharging + + See https://electronics.stackexchange.com/questions/178605/op-amp-mosfet-constant-current-power-source + """ + + def __init__(self, charging_current: RangeLike, sense_resistance: RangeLike, voltage_drop: RangeLike) -> None: + super().__init__() + + self.charging_current = self.ArgParameter(charging_current) + self.sense_resistance = self.ArgParameter(sense_resistance) + self.voltage_drop = self.ArgParameter(voltage_drop) + + self.pwr = self.Port(VoltageSink.empty(), [Power, Input]) + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) + self.require( + self.pwr.current_draw.within( + self.pwr_out.link().current_drawn + (0, self.charging_current.upper()) + (0, 0.05) + ) + ) # TODO nonhacky bounds on opamp/sense resistor current draw + self.sc_out = self.Port(VoltageSource.empty(), optional=True) + self.gnd = self.Port(Ground.empty(), [Common]) + + max_in_voltage = self.pwr.link().voltage.upper() + max_charge_current = self.charging_current.upper() + + # Upstream power domain + # TODO improve connect modeling everywhere + with self.implicit_connect( + ImplicitConnect(self.pwr, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.sense = self.Block( + Resistor( # TODO replace with SeriesResistor/CurrentSenseResistor - that propagates current + resistance=self.sense_resistance, + power=(0, max_charge_current * max_charge_current * self.sense_resistance.upper()), + ) + ) + self.connect(self.pwr, self.sense.a.adapt_to(VoltageSink(current_draw=(0, max_charge_current)))) + + self.fet = self.Block( + Fet.PFet( + drain_voltage=(0, max_in_voltage), + drain_current=(0, max_charge_current), + gate_voltage=(self.pwr.link().voltage.lower(), max_in_voltage), + rds_on=(0, 0.5) * Ohm, # TODO kind of arbitrary + power=(0, max_in_voltage * max_charge_current), + ) + ) + self.connect(self.fet.source, self.sense.b) + + self.diode = self.Block( + Diode( + reverse_voltage=(0, max_in_voltage), + current=self.charging_current, + voltage_drop=self.voltage_drop, + ) + ) + self.connect( + self.diode.anode.adapt_to(VoltageSink()), + self.fet.drain.adapt_to(VoltageSource(voltage_out=self.pwr.link().voltage)), + self.sc_out, + ) + + self.pwr_out_merge = self.Block(MergedVoltageSource()).connected_from( + self.pwr, + self.diode.cathode.adapt_to( + VoltageSource( + voltage_out=( + self.pwr.link().voltage.lower() - self.voltage_drop.upper(), + self.pwr.link().voltage.upper(), + ) + ) + ), # TODO replace with SeriesVoltageDiode or something that automatically calculates voltage drops? + ) + self.connect(self.pwr_out_merge.pwr_out, self.pwr_out) + + # TODO check if this tolerance stackup is stacking in the right direction... it might not + low_sense_volt_diff = self.charging_current.lower() * self.sense_resistance.lower() + high_sense_volt_diff = self.charging_current.upper() * self.sense_resistance.upper() + low_sense_volt = self.pwr.link().voltage.lower() - high_sense_volt_diff + high_sense_volt = self.pwr.link().voltage.upper() - low_sense_volt_diff + + self.set = imp.Block( + VoltageDivider(output_voltage=(low_sense_volt, high_sense_volt), impedance=(1, 10) * kOhm) + ) + self.connect(self.set.input, self.pwr) # TODO use chain + self.amp = imp.Block(Opamp()) + self.connect(self.set.output, self.amp.inp) + self.connect( + self.amp.inn, + self.sense.b.adapt_to( + AnalogSource( + voltage_out=(0, self.pwr.link().voltage.upper()), + # TODO calculate operating signal level + ) + ), + ) + self.connect(self.amp.out, self.fet.gate.adapt_to(AnalogSink())) + + self.cap = self.Block(Supercap()) + self.connect(self.sc_out, self.cap.pos) + self.connect(self.gnd, self.cap.neg) class SingleDiodePowerMerge(PowerConditioner, Block): - """Single-diode power merge block for two voltage sources, where the lower voltage one is diode-gated and less - preferred if both are connected. - """ - def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = RangeExpr.ALL) -> None: - super().__init__() - - self.pwr_in = self.Port(VoltageSink.empty()) # high-priority source - self.pwr_in_diode = self.Port(VoltageSink.empty()) # low-priority source - self.pwr_out = self.Port(VoltageSource.empty()) - - self.diode = self.Block(Diode( - reverse_voltage=(0, self.pwr_in.link().voltage.upper() - self.pwr_in_diode.link().voltage.lower()), - current=self.pwr_out.link().current_drawn, - voltage_drop=voltage_drop, - reverse_recovery_time=reverse_recovery_time, - )) - - self.require(self.pwr_in_diode.link().voltage.upper() - self.diode.voltage_drop.lower() <= self.pwr_in.link().voltage.lower()) - - self.connect(self.pwr_in_diode, self.diode.anode.adapt_to(VoltageSink( - current_draw=self.pwr_out.link().current_drawn - ))) - - self.merge = self.Block(MergedVoltageSource()).connected_from( - self.pwr_in, - self.diode.cathode.adapt_to(VoltageSource( - voltage_out=(self.pwr_in_diode.link().voltage.lower() - self.diode.voltage_drop.upper(), - self.pwr_in_diode.link().voltage.upper() - self.diode.voltage_drop.lower()) - )) - ) - self.connect(self.merge.pwr_out, self.pwr_out) + """Single-diode power merge block for two voltage sources, where the lower voltage one is diode-gated and less + preferred if both are connected. + """ + + def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + + self.pwr_in = self.Port(VoltageSink.empty()) # high-priority source + self.pwr_in_diode = self.Port(VoltageSink.empty()) # low-priority source + self.pwr_out = self.Port(VoltageSource.empty()) + + self.diode = self.Block( + Diode( + reverse_voltage=(0, self.pwr_in.link().voltage.upper() - self.pwr_in_diode.link().voltage.lower()), + current=self.pwr_out.link().current_drawn, + voltage_drop=voltage_drop, + reverse_recovery_time=reverse_recovery_time, + ) + ) + + self.require( + self.pwr_in_diode.link().voltage.upper() - self.diode.voltage_drop.lower() + <= self.pwr_in.link().voltage.lower() + ) + + self.connect( + self.pwr_in_diode, self.diode.anode.adapt_to(VoltageSink(current_draw=self.pwr_out.link().current_drawn)) + ) + + self.merge = self.Block(MergedVoltageSource()).connected_from( + self.pwr_in, + self.diode.cathode.adapt_to( + VoltageSource( + voltage_out=( + self.pwr_in_diode.link().voltage.lower() - self.diode.voltage_drop.upper(), + self.pwr_in_diode.link().voltage.upper() - self.diode.voltage_drop.lower(), + ) + ) + ), + ) + self.connect(self.merge.pwr_out, self.pwr_out) class DiodePowerMerge(PowerConditioner, Block): - """Diode power merge block for two voltage sources. - """ - def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = (0, float('inf'))) -> None: - super().__init__() - - self.pwr_in1 = self.Port(VoltageSink.empty()) - self.pwr_in2 = self.Port(VoltageSink.empty()) - self.pwr_out = self.Port(VoltageSource.empty()) - - output_lower = self.pwr_in1.link().voltage.lower().min(self.pwr_in2.link().voltage.lower()) - RangeExpr._to_expr_type(voltage_drop).upper() - self.diode1 = self.Block(Diode( - reverse_voltage=(0, self.pwr_in1.link().voltage.upper() - output_lower), - current=self.pwr_out.link().current_drawn, - voltage_drop=voltage_drop, - reverse_recovery_time=reverse_recovery_time, - )) - self.diode2 = self.Block(Diode( - reverse_voltage=(0, self.pwr_in2.link().voltage.upper() - output_lower), - current=self.pwr_out.link().current_drawn, - voltage_drop=voltage_drop, - reverse_recovery_time=reverse_recovery_time, - )) - - self.merge = self.Block(MergedVoltageSource()).connected_from( - self.diode1.cathode.adapt_to(VoltageSource( - voltage_out=(self.pwr_in1.link().voltage.lower() - self.diode1.voltage_drop.upper(), - self.pwr_in1.link().voltage.upper()) - )), - self.diode2.cathode.adapt_to(VoltageSource( - voltage_out=(self.pwr_in2.link().voltage.lower() - self.diode2.voltage_drop.upper(), - self.pwr_in2.link().voltage.upper()) - )) - ) - self.connect(self.diode1.anode.adapt_to(VoltageSink( - current_draw=self.pwr_out.link().current_drawn - )), self.pwr_in1) - self.connect(self.diode2.anode.adapt_to(VoltageSink( - current_draw=self.pwr_out.link().current_drawn - )), self.pwr_in2) - - self.connect(self.merge.pwr_out, self.pwr_out) + """Diode power merge block for two voltage sources.""" + + def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = (0, float("inf"))) -> None: + super().__init__() + + self.pwr_in1 = self.Port(VoltageSink.empty()) + self.pwr_in2 = self.Port(VoltageSink.empty()) + self.pwr_out = self.Port(VoltageSource.empty()) + + output_lower = ( + self.pwr_in1.link().voltage.lower().min(self.pwr_in2.link().voltage.lower()) + - RangeExpr._to_expr_type(voltage_drop).upper() + ) + self.diode1 = self.Block( + Diode( + reverse_voltage=(0, self.pwr_in1.link().voltage.upper() - output_lower), + current=self.pwr_out.link().current_drawn, + voltage_drop=voltage_drop, + reverse_recovery_time=reverse_recovery_time, + ) + ) + self.diode2 = self.Block( + Diode( + reverse_voltage=(0, self.pwr_in2.link().voltage.upper() - output_lower), + current=self.pwr_out.link().current_drawn, + voltage_drop=voltage_drop, + reverse_recovery_time=reverse_recovery_time, + ) + ) + + self.merge = self.Block(MergedVoltageSource()).connected_from( + self.diode1.cathode.adapt_to( + VoltageSource( + voltage_out=( + self.pwr_in1.link().voltage.lower() - self.diode1.voltage_drop.upper(), + self.pwr_in1.link().voltage.upper(), + ) + ) + ), + self.diode2.cathode.adapt_to( + VoltageSource( + voltage_out=( + self.pwr_in2.link().voltage.lower() - self.diode2.voltage_drop.upper(), + self.pwr_in2.link().voltage.upper(), + ) + ) + ), + ) + self.connect( + self.diode1.anode.adapt_to(VoltageSink(current_draw=self.pwr_out.link().current_drawn)), self.pwr_in1 + ) + self.connect( + self.diode2.anode.adapt_to(VoltageSink(current_draw=self.pwr_out.link().current_drawn)), self.pwr_in2 + ) + + self.connect(self.merge.pwr_out, self.pwr_out) class PriorityPowerOr(PowerConditioner, KiCadSchematicBlock, Block): - """Power merge block for two power inputs, where the high priority input (e.g. USB) is higher voltage and - the low priority input is lower voltage (e.g. battery). - The higher priority input incurs a diode drop, while the lower priority input has a FET. - As a side effect, the FET power path also acts as reverse polarity protection. - """ - def __init__(self, diode_voltage_drop: RangeLike, fet_rds_on: RangeLike) -> None: - super().__init__() - - self.gnd = self.Port(Ground.empty()) - self.pwr_hi = self.Port(VoltageSink.empty()) # high-priority higher-voltage source - self.pwr_lo = self.Port(VoltageSink.empty()) # low-priority lower-voltage source - self.pwr_out = self.Port(VoltageSource.empty()) - - self.diode_voltage_drop = self.ArgParameter(diode_voltage_drop) - self.fet_rds_on = self.ArgParameter(fet_rds_on) - - @override - def contents(self) -> None: - super().contents() - - # FET behavior requires the high priority path to be higher voltage - self.require(self.pwr_hi.link().voltage.lower() > self.pwr_lo.link().voltage.upper()) - - output_current_draw = self.pwr_out.link().current_drawn - self.pdr = self.Block(Resistor(10*kOhm(tol=0.01))) - self.diode = self.Block(Diode( - reverse_voltage=(0, self.pwr_lo.link().voltage.upper()), - current=output_current_draw, - voltage_drop=self.diode_voltage_drop, - )) - self.fet = self.Block(Fet.PFet( # doesn't account for reverse protection - drain_voltage=(0, self.pwr_hi.link().voltage.upper() - self.pwr_lo.link().voltage.lower()), - drain_current=output_current_draw, - # gate voltage accounts for a possible power on transient - gate_voltage=(-self.pwr_hi.link().voltage.upper(), (self.pwr_hi.link().voltage.upper())), - rds_on=self.fet_rds_on - )) - - self.import_kicad( - self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'pwr_hi': VoltageSink( - current_draw=output_current_draw - ), - 'pwr_lo': VoltageSink( - current_draw=output_current_draw - ), - 'pwr_out': VoltageSource( - voltage_out=self.pwr_lo.link().voltage.hull( - # use the spec voltage drop since using the actual voltage drop causes a circular dependency - # (where current depends on voltage, but the diode voltage drop depends on the diode selection - # which depends on the current through) - self.pwr_hi.link().voltage - self.diode_voltage_drop), - ), - 'gnd': Ground(), - }) - - def connected_from(self, gnd: Optional[Port[GroundLink]] = None, pwr_hi: Optional[Port[VoltageLink]] = None, - pwr_lo: Optional[Port[VoltageLink]] = None) -> 'PriorityPowerOr': - """Convenience function to connect ports, returning this object so it can still be given a name.""" - if gnd is not None: - cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) - if pwr_hi is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_hi, self.pwr_hi) - if pwr_lo is not None: - cast(Block, builder.get_enclosing_block()).connect(pwr_lo, self.pwr_lo) - return self + """Power merge block for two power inputs, where the high priority input (e.g. USB) is higher voltage and + the low priority input is lower voltage (e.g. battery). + The higher priority input incurs a diode drop, while the lower priority input has a FET. + As a side effect, the FET power path also acts as reverse polarity protection. + """ + + def __init__(self, diode_voltage_drop: RangeLike, fet_rds_on: RangeLike) -> None: + super().__init__() + + self.gnd = self.Port(Ground.empty()) + self.pwr_hi = self.Port(VoltageSink.empty()) # high-priority higher-voltage source + self.pwr_lo = self.Port(VoltageSink.empty()) # low-priority lower-voltage source + self.pwr_out = self.Port(VoltageSource.empty()) + + self.diode_voltage_drop = self.ArgParameter(diode_voltage_drop) + self.fet_rds_on = self.ArgParameter(fet_rds_on) + + @override + def contents(self) -> None: + super().contents() + + # FET behavior requires the high priority path to be higher voltage + self.require(self.pwr_hi.link().voltage.lower() > self.pwr_lo.link().voltage.upper()) + + output_current_draw = self.pwr_out.link().current_drawn + self.pdr = self.Block(Resistor(10 * kOhm(tol=0.01))) + self.diode = self.Block( + Diode( + reverse_voltage=(0, self.pwr_lo.link().voltage.upper()), + current=output_current_draw, + voltage_drop=self.diode_voltage_drop, + ) + ) + self.fet = self.Block( + Fet.PFet( # doesn't account for reverse protection + drain_voltage=(0, self.pwr_hi.link().voltage.upper() - self.pwr_lo.link().voltage.lower()), + drain_current=output_current_draw, + # gate voltage accounts for a possible power on transient + gate_voltage=(-self.pwr_hi.link().voltage.upper(), (self.pwr_hi.link().voltage.upper())), + rds_on=self.fet_rds_on, + ) + ) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "pwr_hi": VoltageSink(current_draw=output_current_draw), + "pwr_lo": VoltageSink(current_draw=output_current_draw), + "pwr_out": VoltageSource( + voltage_out=self.pwr_lo.link().voltage.hull( + # use the spec voltage drop since using the actual voltage drop causes a circular dependency + # (where current depends on voltage, but the diode voltage drop depends on the diode selection + # which depends on the current through) + self.pwr_hi.link().voltage + - self.diode_voltage_drop + ), + ), + "gnd": Ground(), + }, + ) + + def connected_from( + self, + gnd: Optional[Port[GroundLink]] = None, + pwr_hi: Optional[Port[VoltageLink]] = None, + pwr_lo: Optional[Port[VoltageLink]] = None, + ) -> "PriorityPowerOr": + """Convenience function to connect ports, returning this object so it can still be given a name.""" + if gnd is not None: + cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd) + if pwr_hi is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_hi, self.pwr_hi) + if pwr_lo is not None: + cast(Block, builder.get_enclosing_block()).connect(pwr_lo, self.pwr_lo) + return self class PmosReverseProtection(PowerConditioner, KiCadSchematicBlock, Block): - """Reverse polarity protection using a PMOS. This method has lower power loss over diode-based protection. - 100R-330R is good but 1k-50k can be used for continuous load. - Ref: https://components101.com/articles/design-guide-pmos-mosfet-for-reverse-voltage-polarity-protection - """ - def __init__(self, gate_resistor: RangeLike = 10 * kOhm(tol=0.05), rds_on: RangeLike = (0, 0.1) * Ohm): - super().__init__() - self.gnd = self.Port(Ground.empty(), [Common]) - self.pwr_in = self.Port(VoltageSink.empty(), [Input]) - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) - - self.gate_resistor = self.ArgParameter(gate_resistor) - self.rds_on = self.ArgParameter(rds_on) - - @override - def contents(self) -> None: - super().contents() - output_current_draw = self.pwr_out.link().current_drawn - self.fet = self.Block(Fet.PFet( - drain_voltage=(0, self.pwr_out.link().voltage.upper()), - drain_current=output_current_draw, - gate_voltage=(-self.pwr_out.link().voltage.upper(), self.pwr_out.link().voltage.upper()), - rds_on=self.rds_on, - )) - - # TODO: generate zener diode + resistor for high voltage applications - # self.diode = self.Block(ZenerDiode(self.clamp_voltage)) - - self.import_kicad( - self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'pwr_in': VoltageSink( - current_draw=output_current_draw, - ), - 'pwr_out': VoltageSource( - voltage_out=self.pwr_in.link().voltage, - ), - 'gnd': Ground(), - }) + """Reverse polarity protection using a PMOS. This method has lower power loss over diode-based protection. + 100R-330R is good but 1k-50k can be used for continuous load. + Ref: https://components101.com/articles/design-guide-pmos-mosfet-for-reverse-voltage-polarity-protection + """ + + def __init__(self, gate_resistor: RangeLike = 10 * kOhm(tol=0.05), rds_on: RangeLike = (0, 0.1) * Ohm): + super().__init__() + self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr_in = self.Port(VoltageSink.empty(), [Input]) + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) + + self.gate_resistor = self.ArgParameter(gate_resistor) + self.rds_on = self.ArgParameter(rds_on) + + @override + def contents(self) -> None: + super().contents() + output_current_draw = self.pwr_out.link().current_drawn + self.fet = self.Block( + Fet.PFet( + drain_voltage=(0, self.pwr_out.link().voltage.upper()), + drain_current=output_current_draw, + gate_voltage=(-self.pwr_out.link().voltage.upper(), self.pwr_out.link().voltage.upper()), + rds_on=self.rds_on, + ) + ) + + # TODO: generate zener diode + resistor for high voltage applications + # self.diode = self.Block(ZenerDiode(self.clamp_voltage)) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "pwr_in": VoltageSink( + current_draw=output_current_draw, + ), + "pwr_out": VoltageSource( + voltage_out=self.pwr_in.link().voltage, + ), + "gnd": Ground(), + }, + ) class PmosChargerReverseProtection(PowerConditioner, KiCadSchematicBlock, Block): - """Charging capable a battery reverse protection using PMOS transistors. The highest battery voltage is bounded by the - transistors' Vgs/Vds. There is also a rare case when this circuit being disconnected when a charger is connected first. - But always reverse protect. R1 and R2 are the pullup bias resistors for mp1 and mp2 PFet. - More info at: https://www.edn.com/reverse-voltage-protection-for-battery-chargers/ - """ - def __init__(self, r1_val: RangeLike = 100 * kOhm(tol=0.01), r2_val: RangeLike = 100 * kOhm(tol=0.01), - rds_on: RangeLike = (0, 0.1) * Ohm): - super().__init__() - - self.gnd = self.Port(Ground.empty(), [Common]) - self.pwr_out = self.Port(VoltageSource.empty(), doc="Power output for a load which will be also reverse protected from the battery") - self.pwr_in = self.Port(VoltageSink.empty(), doc="Power input from the battery") - self.chg_in = self.Port(VoltageSink.empty(), doc="Charger input to charge the battery. Must be connected to pwr_out.") - self.chg_out = self.Port(VoltageSource.empty(), doc="Charging output to the battery chg port. Must be connected to pwr_in,") - - self.r1_val = self.ArgParameter(r1_val) - self.r2_val = self.ArgParameter(r2_val) - self.rds_on = self.ArgParameter(rds_on) - - @override - def contents(self) -> None: - super().contents() - self.r1 = self.Block(Resistor(resistance=self.r1_val)) - self.r2 = self.Block(Resistor(resistance=self.r2_val)) - - batt_voltage = self.pwr_in.link().voltage.hull(self.chg_in.link().voltage).hull(self.gnd.link().voltage) - - # taking the max of the current for the both direction. 0 lower bound - batt_current = self.pwr_out.link().current_drawn.hull(self.chg_out.link().current_drawn).hull((0, 0)) - power = batt_current * batt_current * self.rds_on - r1_current = batt_voltage / self.r1.resistance - - # Create the PMOS transistors and resistors based on the provided schematic - self.mp1 = self.Block(Fet.PFet( - drain_voltage=batt_voltage, drain_current=r1_current, - gate_voltage=(-batt_voltage.upper(), batt_voltage.upper()), - rds_on=self.rds_on, - )) - self.mp2 = self.Block(Fet.PFet( - drain_voltage=batt_voltage, drain_current=batt_current, - gate_voltage=(- batt_voltage.upper(), batt_voltage.upper()), - rds_on=self.rds_on, - power=power - )) - - chg_in_adapter = self.Block(PassiveAdapterVoltageSink()) - setattr(self, '(adapter)chg_in', chg_in_adapter) # hack so the netlister recognizes this as an adapter - self.connect(self.mp1.source, chg_in_adapter.src) - self.connect(self.chg_in, chg_in_adapter.dst) - - chg_out_adapter = self.Block(PassiveAdapterVoltageSource()) - setattr(self, '(adapter)chg_out', chg_out_adapter) # hack so the netlister recognizes this as an adapter - self.connect(self.r2.b, chg_out_adapter.src) - self.connect(self.chg_out, chg_out_adapter.dst) - - self.import_kicad( - self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'pwr_in': VoltageSink( - current_draw=batt_current - ), - 'pwr_out': VoltageSource( - voltage_out=batt_voltage - ), - 'gnd': Ground(), - }) + """Charging capable a battery reverse protection using PMOS transistors. The highest battery voltage is bounded by the + transistors' Vgs/Vds. There is also a rare case when this circuit being disconnected when a charger is connected first. + But always reverse protect. R1 and R2 are the pullup bias resistors for mp1 and mp2 PFet. + More info at: https://www.edn.com/reverse-voltage-protection-for-battery-chargers/ + """ + + def __init__( + self, + r1_val: RangeLike = 100 * kOhm(tol=0.01), + r2_val: RangeLike = 100 * kOhm(tol=0.01), + rds_on: RangeLike = (0, 0.1) * Ohm, + ): + super().__init__() + + self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr_out = self.Port( + VoltageSource.empty(), doc="Power output for a load which will be also reverse protected from the battery" + ) + self.pwr_in = self.Port(VoltageSink.empty(), doc="Power input from the battery") + self.chg_in = self.Port( + VoltageSink.empty(), doc="Charger input to charge the battery. Must be connected to pwr_out." + ) + self.chg_out = self.Port( + VoltageSource.empty(), doc="Charging output to the battery chg port. Must be connected to pwr_in," + ) + + self.r1_val = self.ArgParameter(r1_val) + self.r2_val = self.ArgParameter(r2_val) + self.rds_on = self.ArgParameter(rds_on) + + @override + def contents(self) -> None: + super().contents() + self.r1 = self.Block(Resistor(resistance=self.r1_val)) + self.r2 = self.Block(Resistor(resistance=self.r2_val)) + + batt_voltage = self.pwr_in.link().voltage.hull(self.chg_in.link().voltage).hull(self.gnd.link().voltage) + + # taking the max of the current for the both direction. 0 lower bound + batt_current = self.pwr_out.link().current_drawn.hull(self.chg_out.link().current_drawn).hull((0, 0)) + power = batt_current * batt_current * self.rds_on + r1_current = batt_voltage / self.r1.resistance + + # Create the PMOS transistors and resistors based on the provided schematic + self.mp1 = self.Block( + Fet.PFet( + drain_voltage=batt_voltage, + drain_current=r1_current, + gate_voltage=(-batt_voltage.upper(), batt_voltage.upper()), + rds_on=self.rds_on, + ) + ) + self.mp2 = self.Block( + Fet.PFet( + drain_voltage=batt_voltage, + drain_current=batt_current, + gate_voltage=(-batt_voltage.upper(), batt_voltage.upper()), + rds_on=self.rds_on, + power=power, + ) + ) + + chg_in_adapter = self.Block(PassiveAdapterVoltageSink()) + setattr(self, "(adapter)chg_in", chg_in_adapter) # hack so the netlister recognizes this as an adapter + self.connect(self.mp1.source, chg_in_adapter.src) + self.connect(self.chg_in, chg_in_adapter.dst) + + chg_out_adapter = self.Block(PassiveAdapterVoltageSource()) + setattr(self, "(adapter)chg_out", chg_out_adapter) # hack so the netlister recognizes this as an adapter + self.connect(self.r2.b, chg_out_adapter.src) + self.connect(self.chg_out, chg_out_adapter.dst) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "pwr_in": VoltageSink(current_draw=batt_current), + "pwr_out": VoltageSource(voltage_out=batt_voltage), + "gnd": Ground(), + }, + ) class SoftPowerGate(PowerSwitch, KiCadSchematicBlock, Block): # migrate from the multimater - """A high-side PFET power gate that has a button to power on, can be latched on by an external signal, - and provides the button output as a signal. - """ - def __init__(self, pull_resistance: RangeLike = 10 * kOhm(tol=0.05), amp_resistance: RangeLike = 10 * kOhm(tol=0.05), - diode_drop: RangeLike = (0, 0.4) * Volt): - super().__init__() - self.pwr_in = self.Port(VoltageSink.empty(), [Input]) - self.pwr_out = self.Port(VoltageSource.empty(), [Output], doc="Gate controlled power out") - self.gnd = self.Port(Ground.empty(), [Common]) - - self.btn_out = self.Port(DigitalSource.empty(), optional=True, - doc="Allows the button state to be read independently of the control signal. Open-drain.") - self.btn_in = self.Port(DigitalBidir.empty(), doc="Should be connected to a button output. Do not connect IO") - self.control = self.Port(DigitalSink.empty(), doc="external control to latch the power on") # digital level control - gnd-referenced NFET gate - - self.pull_resistance = self.ArgParameter(pull_resistance) - self.amp_resistance = self.ArgParameter(amp_resistance) - self.diode_drop = self.ArgParameter(diode_drop) - - @override - def contents(self) -> None: - super().contents() - control_voltage = self.btn_in.link().voltage.hull(self.gnd.link().voltage) - pwr_voltage = self.pwr_out.link().voltage.hull(self.gnd.link().voltage) - pwr_current = self.pwr_out.link().current_drawn.hull(RangeExpr.ZERO) - - self.pull_res = self.Block(Resistor( - resistance=self.pull_resistance - )) - self.pwr_fet = self.Block(Fet.PFet( - drain_voltage=pwr_voltage, - drain_current=pwr_current, - gate_voltage=(-control_voltage.upper(), control_voltage.upper()), # TODO this ignores the diode drop - )) - - self.amp_res = self.Block(Resistor( - resistance=self.amp_resistance - )) - self.amp_fet = self.Block(Fet.NFet( - drain_voltage=control_voltage, - drain_current=RangeExpr.ZERO, # effectively no current - gate_voltage=(self.control.link().output_thresholds.upper(), self.control.link().voltage.upper()) - )) - - self.ctl_diode = self.Block(Diode( - reverse_voltage=control_voltage, - current=RangeExpr.ZERO, # effectively no current - voltage_drop=self.diode_drop, - reverse_recovery_time=RangeExpr.ALL - )) - - self.btn_diode = self.Block(Diode( - reverse_voltage=control_voltage, - current=RangeExpr.ZERO, # effectively no current - voltage_drop=self.diode_drop, - reverse_recovery_time=RangeExpr.ALL - )) - - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'pwr_in': VoltageSink( - current_draw=self.pwr_out.link().current_drawn, - voltage_limits=RangeExpr.ALL, - ), - 'pwr_out': VoltageSource( - voltage_out=self.pwr_in.link().voltage, - current_limits=RangeExpr.ALL, - ), - 'control': DigitalSink(), # TODO more modeling here? - 'gnd': Ground(), - 'btn_out': DigitalSource.low_from_supply(self.gnd), - 'btn_in': DigitalBidir( - voltage_out=self.gnd.link().voltage, - output_thresholds=(self.gnd.link().voltage.upper(), float('inf')), - pullup_capable=True, - ) - }) + """A high-side PFET power gate that has a button to power on, can be latched on by an external signal, + and provides the button output as a signal. + """ + + def __init__( + self, + pull_resistance: RangeLike = 10 * kOhm(tol=0.05), + amp_resistance: RangeLike = 10 * kOhm(tol=0.05), + diode_drop: RangeLike = (0, 0.4) * Volt, + ): + super().__init__() + self.pwr_in = self.Port(VoltageSink.empty(), [Input]) + self.pwr_out = self.Port(VoltageSource.empty(), [Output], doc="Gate controlled power out") + self.gnd = self.Port(Ground.empty(), [Common]) + + self.btn_out = self.Port( + DigitalSource.empty(), + optional=True, + doc="Allows the button state to be read independently of the control signal. Open-drain.", + ) + self.btn_in = self.Port(DigitalBidir.empty(), doc="Should be connected to a button output. Do not connect IO") + self.control = self.Port( + DigitalSink.empty(), doc="external control to latch the power on" + ) # digital level control - gnd-referenced NFET gate + + self.pull_resistance = self.ArgParameter(pull_resistance) + self.amp_resistance = self.ArgParameter(amp_resistance) + self.diode_drop = self.ArgParameter(diode_drop) + + @override + def contents(self) -> None: + super().contents() + control_voltage = self.btn_in.link().voltage.hull(self.gnd.link().voltage) + pwr_voltage = self.pwr_out.link().voltage.hull(self.gnd.link().voltage) + pwr_current = self.pwr_out.link().current_drawn.hull(RangeExpr.ZERO) + + self.pull_res = self.Block(Resistor(resistance=self.pull_resistance)) + self.pwr_fet = self.Block( + Fet.PFet( + drain_voltage=pwr_voltage, + drain_current=pwr_current, + gate_voltage=(-control_voltage.upper(), control_voltage.upper()), # TODO this ignores the diode drop + ) + ) + + self.amp_res = self.Block(Resistor(resistance=self.amp_resistance)) + self.amp_fet = self.Block( + Fet.NFet( + drain_voltage=control_voltage, + drain_current=RangeExpr.ZERO, # effectively no current + gate_voltage=(self.control.link().output_thresholds.upper(), self.control.link().voltage.upper()), + ) + ) + + self.ctl_diode = self.Block( + Diode( + reverse_voltage=control_voltage, + current=RangeExpr.ZERO, # effectively no current + voltage_drop=self.diode_drop, + reverse_recovery_time=RangeExpr.ALL, + ) + ) + + self.btn_diode = self.Block( + Diode( + reverse_voltage=control_voltage, + current=RangeExpr.ZERO, # effectively no current + voltage_drop=self.diode_drop, + reverse_recovery_time=RangeExpr.ALL, + ) + ) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "pwr_in": VoltageSink( + current_draw=self.pwr_out.link().current_drawn, + voltage_limits=RangeExpr.ALL, + ), + "pwr_out": VoltageSource( + voltage_out=self.pwr_in.link().voltage, + current_limits=RangeExpr.ALL, + ), + "control": DigitalSink(), # TODO more modeling here? + "gnd": Ground(), + "btn_out": DigitalSource.low_from_supply(self.gnd), + "btn_in": DigitalBidir( + voltage_out=self.gnd.link().voltage, + output_thresholds=(self.gnd.link().voltage.upper(), float("inf")), + pullup_capable=True, + ), + }, + ) class SoftPowerSwitch(PowerSwitch, Block): - """A software power switch that adds a power button a user can turn on - """ - def __init__(self, pull_resistance: RangeLike = 10 * kOhm(tol=0.05), amp_resistance: RangeLike = 10 * kOhm(tol=0.05), - diode_drop: RangeLike = (0, 0.4) * Volt): - super().__init__() - self.pwr_gate = self.Block(SoftPowerGate(pull_resistance, amp_resistance, diode_drop)) - self.gnd = self.Export(self.pwr_gate.gnd, [Common]) - self.pwr_in = self.Export(self.pwr_gate.pwr_in, [Input]) - self.pwr_out = self.Export(self.pwr_gate.pwr_out, [Output]) - self.btn_out = self.Export(self.pwr_gate.btn_out) - self.control = self.Export(self.pwr_gate.control) - - @override - def contents(self) -> None: - super().contents() - with self.implicit_connect( + """A software power switch that adds a power button a user can turn on""" + + def __init__( + self, + pull_resistance: RangeLike = 10 * kOhm(tol=0.05), + amp_resistance: RangeLike = 10 * kOhm(tol=0.05), + diode_drop: RangeLike = (0, 0.4) * Volt, + ): + super().__init__() + self.pwr_gate = self.Block(SoftPowerGate(pull_resistance, amp_resistance, diode_drop)) + self.gnd = self.Export(self.pwr_gate.gnd, [Common]) + self.pwr_in = self.Export(self.pwr_gate.pwr_in, [Input]) + self.pwr_out = self.Export(self.pwr_gate.pwr_out, [Output]) + self.btn_out = self.Export(self.pwr_gate.btn_out) + self.control = self.Export(self.pwr_gate.control) + + @override + def contents(self) -> None: + super().contents() + with self.implicit_connect( ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.btn = imp.Block(DigitalSwitch()) - self.connect(self.pwr_gate.btn_in, self.btn.out) + ) as imp: + self.btn = imp.Block(DigitalSwitch()) + self.connect(self.pwr_gate.btn_in, self.btn.out) diff --git a/edg/parts/PriceGetter.py b/edg/parts/PriceGetter.py index 816283f1f..fd3a71a36 100644 --- a/edg/parts/PriceGetter.py +++ b/edg/parts/PriceGetter.py @@ -15,8 +15,8 @@ def __init__(self, design: CompiledDesign): @override def visit_block(self, context: TransformUtil.TransformContext, block: edgir.BlockTypes) -> None: - lcsc_part_number = self.design.get_value(context.path.to_tuple() + ('lcsc_part',)) - part = self.design.get_value(context.path.to_tuple() + ('fp_part',)) + lcsc_part_number = self.design.get_value(context.path.to_tuple() + ("lcsc_part",)) + part = self.design.get_value(context.path.to_tuple() + ("fp_part",)) if lcsc_part_number: # non-None (value exists) and nonempty assert isinstance(lcsc_part_number, str) @@ -42,13 +42,13 @@ class GeneratePrice(BaseBackend): @staticmethod def price_list_from_str(full_price_list: str) -> List[Tuple[int, float]]: # returns as [ [lower-bound_1, price_1], [lower-bound_2 (and the previous upperbound), price_2], ... ] - price_and_quantity_groups = full_price_list.split(',') + price_and_quantity_groups = full_price_list.split(",") value: List[Tuple[int, float]] = [] for price_and_quantity in price_and_quantity_groups: - price_and_quantity_list = price_and_quantity.split(':') + price_and_quantity_list = price_and_quantity.split(":") # this has to be 2 because it will split the quantity range & the cost: assert len(price_and_quantity_list) == 2 - quantity_range = price_and_quantity_list[0].split('-') + quantity_range = price_and_quantity_list[0].split("-") # when the length is 1, it's the minimum quantity for a price break (ex: 15000+) assert len(quantity_range) == 1 or len(quantity_range) == 2 value.append((int(quantity_range[0]), float(price_and_quantity_list[1]))) @@ -59,19 +59,21 @@ def price_list_from_str(full_price_list: str) -> List[Tuple[int, float]]: @classmethod def get_price_table(cls) -> Dict[str, str]: if not GeneratePrice.PRICE_TABLE: - main_table = PartsTable.with_source_dir(['JLCPCB SMT Parts Library(20220419).csv'], 'resources')[0] + main_table = PartsTable.with_source_dir(["JLCPCB SMT Parts Library(20220419).csv"], "resources")[0] if not os.path.exists(main_table): - main_table = PartsTable.with_source_dir(['Pruned_JLCPCB SMT Parts Library(20220419).csv'], 'resources')[0] + main_table = PartsTable.with_source_dir(["Pruned_JLCPCB SMT Parts Library(20220419).csv"], "resources")[ + 0 + ] parts_tables = [main_table] - supplementary_table = PartsTable.with_source_dir(['supplemental_price.csv'], 'resources')[0] + supplementary_table = PartsTable.with_source_dir(["supplemental_price.csv"], "resources")[0] if os.path.exists(supplementary_table): parts_tables.append(supplementary_table) for table in parts_tables: - with open(table, 'r', newline='', encoding='gb2312') as csv_file: + with open(table, "r", newline="", encoding="gb2312") as csv_file: csv_reader = csv.reader(csv_file) - next(csv_reader) # to skip the header + next(csv_reader) # to skip the header for row in csv_reader: full_price_list = row[10] if not full_price_list.strip(): # missing price, discard row @@ -85,13 +87,13 @@ def generate_price(self, lcsc_part_number: str, quantity: int) -> Optional[float return None full_price_list = self.price_list_from_str(full_price_str) temp_price = full_price_list[0][1] # sets price to initial amount (the lowest quantity bracket) - for (minimum_quantity, price) in full_price_list: + for minimum_quantity, price in full_price_list: if quantity >= minimum_quantity: temp_price = price return quantity * temp_price @override - def run(self, design: CompiledDesign, args: Dict[str, str]={}) -> List[Tuple[edgir.LocalPath, str]]: + def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[edgir.LocalPath, str]]: assert not args price_list = PartQuantityTransform(design).run() total_price: float = 0 @@ -103,6 +105,4 @@ def run(self, design: CompiledDesign, args: Dict[str, str]={}) -> List[Tuple[edg else: print(lcsc_part_number + " is missing from the price list.") - return [ - (edgir.LocalPath(), str(round(total_price, 2))) - ] + return [(edgir.LocalPath(), str(round(total_price, 2)))] diff --git a/edg/parts/ResetGenerator_Apx803s.py b/edg/parts/ResetGenerator_Apx803s.py index d1f452322..01a6b1971 100644 --- a/edg/parts/ResetGenerator_Apx803s.py +++ b/edg/parts/ResetGenerator_Apx803s.py @@ -8,9 +8,7 @@ class Apx803s_Device(InternalSubcircuit, FootprintBlock, GeneratorBlock, JlcPart def __init__(self, reset_threshold: RangeLike) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vcc = self.Port(VoltageSink( - voltage_limits=(1.0, 5.5)*Volt, - current_draw=(10, 15)*uAmp)) + self.vcc = self.Port(VoltageSink(voltage_limits=(1.0, 5.5) * Volt, current_draw=(10, 15) * uAmp)) self.nreset = self.Port(DigitalSource.low_from_supply(self.gnd), [Output]) self.reset_threshold = self.ArgParameter(reset_threshold) @@ -22,9 +20,9 @@ def generate(self) -> None: super().generate() parts = [ # output range, part number, lcsc # (Range(2.21, 2.30), 'APX803S-23SA-7', ''), - (Range(2.59, 2.67), 'APX803S-26SA-7', 'C526393'), - (Range(2.89, 2.97), 'APX803S-29SA-7', 'C143831'), - (Range(3.04, 3.13), 'APX803S-31SA-7', 'C129757'), + (Range(2.59, 2.67), "APX803S-26SA-7", "C526393"), + (Range(2.89, 2.97), "APX803S-29SA-7", "C143831"), + (Range(3.04, 3.13), "APX803S-31SA-7", "C129757"), # (Range(3.94, 4.06), 'APX803S-40SA', ''), # (Range(4.31, 4.45), 'APX803S-44SA-7', ''), # (Range(4.56, 4.70), 'APX803S-46SA-7', ''), @@ -35,21 +33,23 @@ def generate(self) -> None: self.assign(self.actual_reset_threshold, part_reset_threshold) self.footprint( # -SA package pinning - 'U', 'Package_TO_SOT_SMD:SOT-23', + "U", + "Package_TO_SOT_SMD:SOT-23", { - '1': self.gnd, - '2': self.nreset, - '3': self.vcc, + "1": self.gnd, + "2": self.nreset, + "3": self.vcc, }, - mfr='Diodes Incorporated', part=part_number, - datasheet='https://www.diodes.com/assets/Datasheets/APX803S.pdf' + mfr="Diodes Incorporated", + part=part_number, + datasheet="https://www.diodes.com/assets/Datasheets/APX803S.pdf", ) self.assign(self.lcsc_part, lcsc_part) self.assign(self.actual_basic_part, False) class Apx803s(Interface, Block): - def __init__(self, reset_threshold: RangeLike = (2.88, 2.98)*Volt) -> None: # expanded range for FP precision + def __init__(self, reset_threshold: RangeLike = (2.88, 2.98) * Volt) -> None: # expanded range for FP precision super().__init__() self.ic = self.Block(Apx803s_Device(reset_threshold)) # datasheet doesn't require decaps self.pwr = self.Export(self.ic.vcc, [Power]) diff --git a/edg/parts/ResistiveSensor.py b/edg/parts/ResistiveSensor.py index 3cd194790..a32234c1c 100644 --- a/edg/parts/ResistiveSensor.py +++ b/edg/parts/ResistiveSensor.py @@ -4,42 +4,42 @@ class ConnectorResistiveSensor(Analog, Block): - """Senses the resistance of an external resistor (through an abstract connector - that is part of this block) using a simple voltage divider circuit. - The external resistor is on the bottom (which makes this of a classic Wheatstone Bridge - as drawn on Wikipedia).""" - def __init__(self, resistance_range: RangeLike, fixed_resistance: RangeLike) -> None: - super().__init__() - self.resistance_range = self.ArgParameter(resistance_range) - self.fixed_resistance = self.ArgParameter(fixed_resistance) - - self.input = self.Port(VoltageSink.empty(), [Power]) - self.output = self.Port(AnalogSource.empty(), [Output]) - self.gnd = self.Port(Ground.empty(), [Common]) - - # TODO deduplicate with ResistiveDivider class - self.actual_ratio = self.Parameter(RangeExpr()) - self.actual_impedance = self.Parameter(RangeExpr()) - self.actual_series_impedance = self.Parameter(RangeExpr()) - - @override - def contents(self) -> None: - self.top = self.Block(Resistor(self.fixed_resistance, voltage=self.input.link().voltage)) - self.bot = self.Block(PassiveConnector(2)) - self.connect(self.input, self.top.a.adapt_to(VoltageSink( - current_draw=self.output.link().current_drawn - ))) - output_voltage = ResistiveDivider.divider_output(self.input.link().voltage, self.gnd.link().voltage, self.actual_ratio) - self.connect(self.output, self.top.b.adapt_to(AnalogSource( - voltage_out=output_voltage, - signal_out=output_voltage, - impedance=self.actual_impedance - )), self.bot.pins.request('1').adapt_to(AnalogSink())) - self.connect(self.gnd, self.bot.pins.request('2').adapt_to(Ground())) - - self.assign(self.actual_impedance, - 1 / (1 / self.top.actual_resistance + 1 / self.resistance_range)) - self.assign(self.actual_series_impedance, - self.top.actual_resistance + self.resistance_range) - self.assign(self.actual_ratio, - 1 / (self.top.actual_resistance / self.resistance_range + 1)) + """Senses the resistance of an external resistor (through an abstract connector + that is part of this block) using a simple voltage divider circuit. + The external resistor is on the bottom (which makes this of a classic Wheatstone Bridge + as drawn on Wikipedia).""" + + def __init__(self, resistance_range: RangeLike, fixed_resistance: RangeLike) -> None: + super().__init__() + self.resistance_range = self.ArgParameter(resistance_range) + self.fixed_resistance = self.ArgParameter(fixed_resistance) + + self.input = self.Port(VoltageSink.empty(), [Power]) + self.output = self.Port(AnalogSource.empty(), [Output]) + self.gnd = self.Port(Ground.empty(), [Common]) + + # TODO deduplicate with ResistiveDivider class + self.actual_ratio = self.Parameter(RangeExpr()) + self.actual_impedance = self.Parameter(RangeExpr()) + self.actual_series_impedance = self.Parameter(RangeExpr()) + + @override + def contents(self) -> None: + self.top = self.Block(Resistor(self.fixed_resistance, voltage=self.input.link().voltage)) + self.bot = self.Block(PassiveConnector(2)) + self.connect(self.input, self.top.a.adapt_to(VoltageSink(current_draw=self.output.link().current_drawn))) + output_voltage = ResistiveDivider.divider_output( + self.input.link().voltage, self.gnd.link().voltage, self.actual_ratio + ) + self.connect( + self.output, + self.top.b.adapt_to( + AnalogSource(voltage_out=output_voltage, signal_out=output_voltage, impedance=self.actual_impedance) + ), + self.bot.pins.request("1").adapt_to(AnalogSink()), + ) + self.connect(self.gnd, self.bot.pins.request("2").adapt_to(Ground())) + + self.assign(self.actual_impedance, 1 / (1 / self.top.actual_resistance + 1 / self.resistance_range)) + self.assign(self.actual_series_impedance, self.top.actual_resistance + self.resistance_range) + self.assign(self.actual_ratio, 1 / (self.top.actual_resistance / self.resistance_range + 1)) diff --git a/edg/parts/RfModules.py b/edg/parts/RfModules.py index 0866a735a..e9571388e 100644 --- a/edg/parts/RfModules.py +++ b/edg/parts/RfModules.py @@ -4,121 +4,140 @@ class Xbee_S3b_Device(InternalSubcircuit, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - # only required pins are Vcc, GND, DOUT, DIN - # also need RTS, DTR for serial firmware updates - # DNC pins that are not in use - self.pwr = self.Port(VoltageSink( - voltage_limits=(2.1, 3.6) * Volt, - current_draw=(0.0025, 290) * mAmp # 2.5uA sleep, 29mA receive, 290 mA max transmit - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - - digital_model = DigitalBidir.from_supply( - self.gnd, self.pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # TODO speculative deafult - current_limits=(-2, 2) * mAmp, - input_threshold_factor=(0.3, 0.7), - output_threshold_factor=(0.05, 0.95) - ) - - self.data = self.Port(UartPort(digital_model), [Input]) - - self.rssi = self.Port(DigitalSource.from_bidir(digital_model), optional=True) - self.associate = self.Port(DigitalSource.from_bidir(digital_model), optional=True) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Sparkfun_RF:XBEE', - { - '1': self.pwr, - '2': self.data.tx, - '3': self.data.rx, - # '4': , # GPIO / SPI SO - # '5': , # reset w/ internal PUR - '6': self.rssi, # GPIO / RSSI - # '7': , # GPIO / PWM - # '8': , # reserved - # '9': , # GPIO / sleep control - '10': self.gnd, - # '11': , # GPIO / CS in - # '12': , # GPIO / CTS - # '13': , # GPIO / module status indicator - '14': self.gnd, # Vref, connected to GND if not using analog sampling - '15': self.associate, # GPIO / associate indicator - # '16': , # GPIO / RTS - # '17': , # GPIO / Ain / CS - # '18': , # GPIO / Ain / SPI SCK - # '19': , # GPIO / Ain / SPI attention - # '20': , # GIO / Ain - }, - mfr='Digi International', part='XBP9B-*', - datasheet='https://www.digi.com/resources/documentation/digidocs/pdfs/90002173.pdf' - ) + def __init__(self) -> None: + super().__init__() + + # only required pins are Vcc, GND, DOUT, DIN + # also need RTS, DTR for serial firmware updates + # DNC pins that are not in use + self.pwr = self.Port( + VoltageSink( + voltage_limits=(2.1, 3.6) * Volt, + current_draw=(0.0025, 290) * mAmp, # 2.5uA sleep, 29mA receive, 290 mA max transmit + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + + digital_model = DigitalBidir.from_supply( + self.gnd, + self.pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, # TODO speculative deafult + current_limits=(-2, 2) * mAmp, + input_threshold_factor=(0.3, 0.7), + output_threshold_factor=(0.05, 0.95), + ) + + self.data = self.Port(UartPort(digital_model), [Input]) + + self.rssi = self.Port(DigitalSource.from_bidir(digital_model), optional=True) + self.associate = self.Port(DigitalSource.from_bidir(digital_model), optional=True) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Sparkfun_RF:XBEE", + { + "1": self.pwr, + "2": self.data.tx, + "3": self.data.rx, + # '4': , # GPIO / SPI SO + # '5': , # reset w/ internal PUR + "6": self.rssi, # GPIO / RSSI + # '7': , # GPIO / PWM + # '8': , # reserved + # '9': , # GPIO / sleep control + "10": self.gnd, + # '11': , # GPIO / CS in + # '12': , # GPIO / CTS + # '13': , # GPIO / module status indicator + "14": self.gnd, # Vref, connected to GND if not using analog sampling + "15": self.associate, # GPIO / associate indicator + # '16': , # GPIO / RTS + # '17': , # GPIO / Ain / CS + # '18': , # GPIO / Ain / SPI SCK + # '19': , # GPIO / Ain / SPI attention + # '20': , # GIO / Ain + }, + mfr="Digi International", + part="XBP9B-*", + datasheet="https://www.digi.com/resources/documentation/digidocs/pdfs/90002173.pdf", + ) class Xbee_S3b(Interface, Radiofrequency, FootprintBlock): - """XBee-PRO 900HP, product numbers XBP9B-*""" - def __init__(self) -> None: - super().__init__() - - self.ic = self.Block(Xbee_S3b_Device()) - self.pwr = self.Export(self.ic.pwr, [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.data = self.Export(self.ic.data) - self.rssi = self.Export(self.ic.rssi, optional=True) - self.associate = self.Export(self.ic.associate, optional=True) - - @override - def contents(self) -> None: - super().contents() - - self.vdd_cap_0 = self.Block(DecouplingCapacitor( - capacitance=1.0*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) - self.vdd_cap_1 = self.Block(DecouplingCapacitor( - capacitance=47*pFarad(tol=0.2), - )).connected(self.gnd, self.pwr) + """XBee-PRO 900HP, product numbers XBP9B-*""" + + def __init__(self) -> None: + super().__init__() + + self.ic = self.Block(Xbee_S3b_Device()) + self.pwr = self.Export(self.ic.pwr, [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.data = self.Export(self.ic.data) + self.rssi = self.Export(self.ic.rssi, optional=True) + self.associate = self.Export(self.ic.associate, optional=True) + + @override + def contents(self) -> None: + super().contents() + + self.vdd_cap_0 = self.Block( + DecouplingCapacitor( + capacitance=1.0 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) + self.vdd_cap_1 = self.Block( + DecouplingCapacitor( + capacitance=47 * pFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) class BlueSmirf(Interface, Radiofrequency, FootprintBlock): - """BlueSMiRF Gold/Silver""" - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSink( - voltage_limits=(3, 6) * Volt, # TODO added a -10% tolerance on the low side so things still work - technically out of spec - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - - digital_model = DigitalBidir.from_supply( - pos=self.pwr, neg=self.gnd, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, # TODO actually an unspecified default - # TODO other parameters given the logic conversion circuit - input_threshold_factor=(0.5, 0.5) # TODO completely wild relaxed unrealistic guess - ) - - self.data = self.Port(UartPort(digital_model), [Input]) - self.cts = self.Port(DigitalSink.from_bidir(digital_model), optional=True) - self.rts = self.Port(DigitalSource.from_bidir(digital_model), optional=True) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical', - { - '1': self.cts, - '2': self.pwr, - '3': self.gnd, - '4': self.data.tx, - '5': self.data.rx, - '6': self.rts, - }, - mfr='Sparkfun', part='BlueSMiRF', - datasheet='https://www.sparkfun.com/products/12577' - ) + """BlueSMiRF Gold/Silver""" + + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port( + VoltageSink( + voltage_limits=(3, 6) + * Volt, # TODO added a -10% tolerance on the low side so things still work - technically out of spec + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + + digital_model = DigitalBidir.from_supply( + pos=self.pwr, + neg=self.gnd, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, # TODO actually an unspecified default + # TODO other parameters given the logic conversion circuit + input_threshold_factor=(0.5, 0.5), # TODO completely wild relaxed unrealistic guess + ) + + self.data = self.Port(UartPort(digital_model), [Input]) + self.cts = self.Port(DigitalSink.from_bidir(digital_model), optional=True) + self.rts = self.Port(DigitalSource.from_bidir(digital_model), optional=True) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical", + { + "1": self.cts, + "2": self.pwr, + "3": self.gnd, + "4": self.data.tx, + "5": self.data.rx, + "6": self.rts, + }, + mfr="Sparkfun", + part="BlueSMiRF", + datasheet="https://www.sparkfun.com/products/12577", + ) diff --git a/edg/parts/Rf_Pn7160.py b/edg/parts/Rf_Pn7160.py index 8f6451bdb..38b5d04c2 100644 --- a/edg/parts/Rf_Pn7160.py +++ b/edg/parts/Rf_Pn7160.py @@ -13,24 +13,35 @@ # TODO: maybe have a RfPort / DifferentialRfPort bidirectional type modeling impedances # TODO: use actual component values in calculations, to account for tolerance stackup + class NfcAntenna(FootprintBlock, GeneratorBlock, Interface): """NFC antenna, also calculates the complex impedance from series-LRC parameters. In this model, the L and R are in series, and the C is in parallel with the LR stack. As in https://www.nxp.com/docs/en/application-note/AN13219.pdf """ + @classmethod - def impedance_from_lrc(cls, freq: float, inductance: float, resistance: float, capacitance: float = 0.1e-9) -> complex: + def impedance_from_lrc( + cls, freq: float, inductance: float, resistance: float, capacitance: float = 0.1e-9 + ) -> complex: """Calculates the complex impedance of this antenna given the antenna L, R, C. A default C of 0.1pF From https://www.eetimes.eu/impedance-matching-for-nfc-applications/""" w = 2 * pi * freq - realpart = resistance / ((1 - (w**2) * inductance * capacitance)**2 + (w * resistance * capacitance)**2) - imagpart = (w * inductance - (w**3) * (inductance**2) * capacitance - w * (resistance**2) * capacitance) / \ - ((1 - (w**2) * inductance * capacitance)**2 + (w * resistance * capacitance)**2) + realpart = resistance / ((1 - (w**2) * inductance * capacitance) ** 2 + (w * resistance * capacitance) ** 2) + imagpart = ( + w * inductance - (w**3) * (inductance**2) * capacitance - w * (resistance**2) * capacitance + ) / ((1 - (w**2) * inductance * capacitance) ** 2 + (w * resistance * capacitance) ** 2) return complex(realpart, imagpart) - def __init__(self, ant_footprint: StringLike, freq: FloatLike, inductance: FloatLike, resistance: FloatLike, - capacitance: FloatLike): + def __init__( + self, + ant_footprint: StringLike, + freq: FloatLike, + inductance: FloatLike, + resistance: FloatLike, + capacitance: FloatLike, + ): super().__init__() self.ant1 = self.Port(Passive()) self.ant2 = self.Port(Passive()) @@ -48,17 +59,18 @@ def __init__(self, ant_footprint: StringLike, freq: FloatLike, inductance: Float def generate(self) -> None: super().generate() - impedance = NfcAntenna.impedance_from_lrc(self.get(self.freq), self.get(self.inductance), - self.get(self.resistance), self.get(self.capacitance)) + impedance = NfcAntenna.impedance_from_lrc( + self.get(self.freq), self.get(self.inductance), self.get(self.resistance), self.get(self.capacitance) + ) self.assign(self.z_real, impedance.real) self.assign(self.z_imag, impedance.imag) - self.footprint('ANT', self.ant_footprint, {'1': self.ant1, '2': self.ant2}) + self.footprint("ANT", self.ant_footprint, {"1": self.ant1, "2": self.ant2}) class NfcAntennaDampening(InternalSubcircuit, GeneratorBlock): - """Differential antenna dampening circuit, two inline resistors to achieve some target Q - """ + """Differential antenna dampening circuit, two inline resistors to achieve some target Q""" + @classmethod def damp_res_from_impedance(self, impedance: complex, target_q: float) -> float: """Calculates the single-ended damping resistance needed to achieve some target Q. @@ -83,9 +95,10 @@ def __init__(self, target_q: FloatLike, ant_r: FloatLike, ant_x: FloatLike): def generate(self) -> None: super().generate() - res_value = self.damp_res_from_impedance(complex(self.get(self.ant_r), self.get(self.ant_x)), - self.get(self.target_q)) - res_model = Resistor((res_value / 2)*Ohm(tol=0.05)) + res_value = self.damp_res_from_impedance( + complex(self.get(self.ant_r), self.get(self.ant_x)), self.get(self.target_q) + ) + res_model = Resistor((res_value / 2) * Ohm(tol=0.05)) self.r1 = self.Block(res_model) self.r2 = self.Block(res_model) self.connect(self.in1, self.r1.a) @@ -99,12 +112,20 @@ def generate(self) -> None: class DifferentialLcLowpassFilter(GeneratorBlock, RfFilter): """Differential LC lowpass filter, commonly used as an EMC filter in the NFC analog frontend Input resistance is used to calculate the output impedance""" + @classmethod def _calculate_capacitance(cls, freq_cutoff: float, inductance: float) -> float: - return 1 / (inductance * (2*pi*freq_cutoff)**2) # from f = 1 / (2 pi sqrt(LC)) - - def __init__(self, freq_cutoff: FloatLike, inductance: FloatLike, input_res: FloatLike, - freq: FloatLike, current: RangeLike, voltage: RangeLike): + return 1 / (inductance * (2 * pi * freq_cutoff) ** 2) # from f = 1 / (2 pi sqrt(LC)) + + def __init__( + self, + freq_cutoff: FloatLike, + inductance: FloatLike, + input_res: FloatLike, + freq: FloatLike, + current: RangeLike, + voltage: RangeLike, + ): super().__init__() self.freq_cutoff = self.ArgParameter(freq_cutoff) self.inductance = self.ArgParameter(inductance) @@ -127,12 +148,13 @@ def __init__(self, freq_cutoff: FloatLike, inductance: FloatLike, input_res: Flo def generate(self) -> None: super().generate() - inductor_model = Inductor(self.get(self.inductance)*Henry(tol=0.1), - current=self.current, frequency=(0, self.freq_cutoff)) + inductor_model = Inductor( + self.get(self.inductance) * Henry(tol=0.1), current=self.current, frequency=(0, self.freq_cutoff) + ) self.l1 = self.Block(inductor_model) self.l2 = self.Block(inductor_model) capacitance = self._calculate_capacitance(self.get(self.freq_cutoff), self.get(self.inductance)) - cap_model = Capacitor(capacitance*Farad(tol=0.2), voltage=self.voltage) + cap_model = Capacitor(capacitance * Farad(tol=0.2), voltage=self.voltage) self.c1 = self.Block(cap_model) self.c2 = self.Block(cap_model) self.connect(self.in1, self.l1.a) @@ -141,8 +163,9 @@ def generate(self) -> None: self.connect(self.l2.b, self.c2.pos, self.out2) self.connect(self.c1.neg.adapt_to(Ground()), self.c2.neg.adapt_to(Ground()), self.gnd) - impedance = NfcAntenna.impedance_from_lrc(self.get(self.freq), self.get(self.inductance), - self.get(self.input_res), capacitance) + impedance = NfcAntenna.impedance_from_lrc( + self.get(self.freq), self.get(self.inductance), self.get(self.input_res), capacitance + ) self.assign(self.z_real, impedance.real) self.assign(self.z_imag, impedance.imag) @@ -156,8 +179,15 @@ def _calculate_se_values(cls, freq: float, z1: complex, z2: complex) -> Tuple[fl se_cp = PiLowPassFilter._reactance_to_capacitance(freq, se_xp) return se_cs, se_cp - def __init__(self, freq: FloatLike, src_r: FloatLike, src_x: FloatLike, snk_r: FloatLike, snk_x: FloatLike, - voltage: RangeLike): + def __init__( + self, + freq: FloatLike, + src_r: FloatLike, + src_x: FloatLike, + snk_r: FloatLike, + snk_x: FloatLike, + voltage: RangeLike, + ): super().__init__() self.freq = self.ArgParameter(freq) self.src_r = self.ArgParameter(src_r) @@ -177,13 +207,15 @@ def __init__(self, freq: FloatLike, src_r: FloatLike, src_x: FloatLike, snk_r: F def generate(self) -> None: super().generate() - diff_cs, diff_cp = self._calculate_se_values(self.get(self.freq), - complex(self.get(self.src_r), self.get(self.src_x)), - complex(self.get(self.snk_r), self.get(self.snk_x))) - cs_model = Capacitor(-diff_cs*2*Farad(tol=0.2), voltage=self.voltage) + diff_cs, diff_cp = self._calculate_se_values( + self.get(self.freq), + complex(self.get(self.src_r), self.get(self.src_x)), + complex(self.get(self.snk_r), self.get(self.snk_x)), + ) + cs_model = Capacitor(-diff_cs * 2 * Farad(tol=0.2), voltage=self.voltage) self.cs1 = self.Block(cs_model) self.cs2 = self.Block(cs_model) - cp_model = Capacitor(-diff_cp*2*Farad(tol=0.2), voltage=self.voltage) + cp_model = Capacitor(-diff_cp * 2 * Farad(tol=0.2), voltage=self.voltage) self.cp1 = self.Block(cp_model) self.cp2 = self.Block(cp_model) self.connect(self.in1, self.cs1.pos) @@ -225,32 +257,35 @@ class Pn7160_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.vss = self.Port(Ground(), [Common]) - self.vbat = self.Port(VoltageSink.from_gnd( - self.vss, - voltage_limits=(2.5, 5.5)*Volt, - current_draw=(0.0105, 290)*mAmp)) # hard power down to continuous transmit, limit of 330mA - self.vddup = self.Port(VoltageSink.from_gnd( - self.vss, - voltage_limits=(2.8, 5.8)*Volt)) - self.vddpad = self.Port(VoltageSink.from_gnd( - self.vss, - voltage_limits=(3.0, 3.6)*Volt)) # also available in 1.8v nominal + self.vbat = self.Port( + VoltageSink.from_gnd(self.vss, voltage_limits=(2.5, 5.5) * Volt, current_draw=(0.0105, 290) * mAmp) + ) # hard power down to continuous transmit, limit of 330mA + self.vddup = self.Port(VoltageSink.from_gnd(self.vss, voltage_limits=(2.8, 5.8) * Volt)) + self.vddpad = self.Port( + VoltageSink.from_gnd(self.vss, voltage_limits=(3.0, 3.6) * Volt) + ) # also available in 1.8v nominal # internally generated supplies - self.vdd = self.Port(VoltageSource( - voltage_out=(1.7, 1.95)*Volt, # Vddd pin characteristics - current_limits=(0, 0)*Amp # connect decap only - )) - self.vddmid = self.Port(VoltageSource( - voltage_out=1.8*Volt(tol=0), # assumed from external capacitor requirement - current_limits=(0, 0)*Amp # connect decap only - )) - self.vddtx = self.Port(VoltageSource( - voltage_out=(self.vddup.link().voltage - 0.3*Volt).hull(2.5*Volt), # up to 0.3v dropout - current_limits=(0, 0)*Amp # connect decap only - )) - - self.xtal = self.Port(CrystalDriver(frequency_limits=27.12*MHertz(tol=50e-6))) + self.vdd = self.Port( + VoltageSource( + voltage_out=(1.7, 1.95) * Volt, # Vddd pin characteristics + current_limits=(0, 0) * Amp, # connect decap only + ) + ) + self.vddmid = self.Port( + VoltageSource( + voltage_out=1.8 * Volt(tol=0), # assumed from external capacitor requirement + current_limits=(0, 0) * Amp, # connect decap only + ) + ) + self.vddtx = self.Port( + VoltageSource( + voltage_out=(self.vddup.link().voltage - 0.3 * Volt).hull(2.5 * Volt), # up to 0.3v dropout + current_limits=(0, 0) * Amp, # connect decap only + ) + ) + + self.xtal = self.Port(CrystalDriver(frequency_limits=27.12 * MHertz(tol=50e-6))) # antenna interface self.tx1 = self.Port(Passive()) @@ -259,73 +294,79 @@ def __init__(self) -> None: self.rxn = self.Port(Passive()) # digital interfaces - self.i2c = self.Port(I2cTarget(DigitalBidir.from_supply( - self.vss, self.vddpad, - input_threshold_factor=(0.3, 0.7) - ), addresses=[0x28])) # in ADR = (0, 0) - - self.irq = self.Port(DigitalSource.from_supply( - self.vss, self.vddpad, - )) # per AN12988 I2C can be polled but IRQ is recommended, but per UM11495 IRQ is required - self.ven = self.Port(DigitalSink.from_supply( # reset - self.vss, self.vbat, - voltage_limit_tolerance=(0, 0), - input_threshold_abs=(0.4, 1.1)*Volt - )) + self.i2c = self.Port( + I2cTarget( + DigitalBidir.from_supply(self.vss, self.vddpad, input_threshold_factor=(0.3, 0.7)), addresses=[0x28] + ) + ) # in ADR = (0, 0) + + self.irq = self.Port( + DigitalSource.from_supply( + self.vss, + self.vddpad, + ) + ) # per AN12988 I2C can be polled but IRQ is recommended, but per UM11495 IRQ is required + self.ven = self.Port( + DigitalSink.from_supply( # reset + self.vss, self.vbat, voltage_limit_tolerance=(0, 0), input_threshold_abs=(0.4, 1.1) * Volt + ) + ) @override def contents(self) -> None: self.footprint( - 'U', 'Package_DFN_QFN:HVQFN-40-1EP_6x6mm_P0.5mm_EP4.1x4.1mm', + "U", + "Package_DFN_QFN:HVQFN-40-1EP_6x6mm_P0.5mm_EP4.1x4.1mm", { - '1': self.vss, # self.i2c_adr0, + "1": self.vss, # self.i2c_adr0, # '2': DWL_REQ, firmware download control, leave open or ground if unused (internal pulldown) - '3': self.vss, # self.i2c_adr1, - '4': self.vss, # Vsspad - '5': self.i2c.sda, - '6': self.vddpad, - '7': self.i2c.scl, - '8': self.irq, - '9': self.vss, # VssA - '10': self.ven, # reset + hard power down + "3": self.vss, # self.i2c_adr1, + "4": self.vss, # Vsspad + "5": self.i2c.sda, + "6": self.vddpad, + "7": self.i2c.scl, + "8": self.irq, + "9": self.vss, # VssA + "10": self.ven, # reset + hard power down # 11 internally connected, leave open - '12': self.vbat, # Vbat2 - '13': self.vddup, - '14': self.vddtx, - '15': self.rxn, - '16': self.rxp, - '17': self.vddmid, - '18': self.vddtx, # TVddIn - '19': self.tx2, - '20': self.vss, # VssTx - '21': self.tx1, - '22': self.vddtx, # TVddIn2 + "12": self.vbat, # Vbat2 + "13": self.vddup, + "14": self.vddtx, + "15": self.rxn, + "16": self.rxp, + "17": self.vddmid, + "18": self.vddtx, # TVddIn + "19": self.tx2, + "20": self.vss, # VssTx + "21": self.tx1, + "22": self.vddtx, # TVddIn2 # '23': self.ant1, # ANT1/2, VddHF are for antenna connection for wake-up, not used # '24': self.ant2, # '25': self.vddhf, - '26': self.vdd, # AVdd - '27': self.vdd, - '28': self.vbat, - '29': self.xtal.xtal_out, # xtal2 - '30': self.xtal.xtal_in, # nfc_clk_xtal1, - '31': self.vdd, # DVdd / Vddd + "26": self.vdd, # AVdd + "27": self.vdd, + "28": self.vbat, + "29": self.xtal.xtal_out, # xtal2 + "30": self.xtal.xtal_in, # nfc_clk_xtal1, + "31": self.vdd, # DVdd / Vddd # 32-36 NC # '37': self.dcdcen, # for external DC-DC mode, connect to enable # 38 NC # '39': self.wkup_req, # optional it seems, wake-up can be done via host interface # '40': self.clk_req, # for clock-in, indicate when clock needs to be driven, unconnected if unused - '41': self.vss, # center pad + "41": self.vss, # center pad }, - mfr='NXP', part='PN7160A1HN/C100Y', - datasheet='https://www.nxp.com/docs/en/data-sheet/PN7160_PN7161.pdf' + mfr="NXP", + part="PN7160A1HN/C100Y", + datasheet="https://www.nxp.com/docs/en/data-sheet/PN7160_PN7161.pdf", ) - self.assign(self.lcsc_part, 'C3303790') + self.assign(self.lcsc_part, "C3303790") self.assign(self.actual_basic_part, False) class Pn7160(Resettable, DiscreteRfWarning, Block): - """Multi-protocol NFC controller, up to 1.3W output power, in I2C ('A' suffix) - """ + """Multi-protocol NFC controller, up to 1.3W output power, in I2C ('A' suffix)""" + def __init__(self) -> None: super().__init__() self.ic = self.Block(Pn7160_Device()) @@ -343,54 +384,75 @@ def contents(self) -> None: self.connect(self.ic.vbat, self.ic.vddup) # CFG1, VddUp and Vbat from same supply with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.gnd, [Common]), ) as imp: # caps table from hardware design guide, 10% or better tolerance recommended - self.cvddup = imp.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.1))).connected(pwr=self.ic.vddup) - self.cvbat = imp.Block(DecouplingCapacitor(capacitance=4.7*uFarad(tol=0.1))).connected(pwr=self.ic.vbat) - self.cvbat1 = imp.Block(DecouplingCapacitor(capacitance=100*nFarad(tol=0.1))).connected(pwr=self.ic.vbat) - self.cvdd1 = imp.Block(DecouplingCapacitor(capacitance=2.2*uFarad(tol=0.1))).connected(pwr=self.ic.vdd) - self.cvdd2 = imp.Block(DecouplingCapacitor(capacitance=2.2*uFarad(tol=0.1))).connected(pwr=self.ic.vdd) - self.ctvdd1 = imp.Block(DecouplingCapacitor(capacitance=2.2*uFarad(tol=0.1))).connected(pwr=self.ic.vddtx) - self.ctvdd2 = imp.Block(DecouplingCapacitor(capacitance=2.2*uFarad(tol=0.1))).connected(pwr=self.ic.vddtx) - self.cvddpad = imp.Block(DecouplingCapacitor(capacitance=1*uFarad(tol=0.1))).connected(pwr=self.ic.vddpad) - self.cvddmid = imp.Block(DecouplingCapacitor(capacitance=100*nFarad(tol=0.1))).connected(pwr=self.ic.vddmid) - - self.xtal = imp.Block(OscillatorReference(27.12*MHertz(tol=50e-6))) # TODO only needed in RF polling mode + self.cvddup = imp.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.1))).connected(pwr=self.ic.vddup) + self.cvbat = imp.Block(DecouplingCapacitor(capacitance=4.7 * uFarad(tol=0.1))).connected(pwr=self.ic.vbat) + self.cvbat1 = imp.Block(DecouplingCapacitor(capacitance=100 * nFarad(tol=0.1))).connected(pwr=self.ic.vbat) + self.cvdd1 = imp.Block(DecouplingCapacitor(capacitance=2.2 * uFarad(tol=0.1))).connected(pwr=self.ic.vdd) + self.cvdd2 = imp.Block(DecouplingCapacitor(capacitance=2.2 * uFarad(tol=0.1))).connected(pwr=self.ic.vdd) + self.ctvdd1 = imp.Block(DecouplingCapacitor(capacitance=2.2 * uFarad(tol=0.1))).connected(pwr=self.ic.vddtx) + self.ctvdd2 = imp.Block(DecouplingCapacitor(capacitance=2.2 * uFarad(tol=0.1))).connected(pwr=self.ic.vddtx) + self.cvddpad = imp.Block(DecouplingCapacitor(capacitance=1 * uFarad(tol=0.1))).connected(pwr=self.ic.vddpad) + self.cvddmid = imp.Block(DecouplingCapacitor(capacitance=100 * nFarad(tol=0.1))).connected( + pwr=self.ic.vddmid + ) + + self.xtal = imp.Block(OscillatorReference(27.12 * MHertz(tol=50e-6))) # TODO only needed in RF polling mode self.connect(self.ic.xtal, self.xtal.crystal) # for symmetrical tuning, 14.4-14.7MHz cutoff, for asymmetrical tuning, 20-22MHz cutoff # 20-ohm differential to TX1-TX2 is a recommendation from the datasheet # while the reference design uses 160nH, this chooses 220nH to align with the E6 series - SIGNAL_FREQ = 13.56*MHertz - CAP_VOLTAGE = (0, 25)*Volt # wild guess, to spec for 50V NP0 after derating + SIGNAL_FREQ = 13.56 * MHertz + CAP_VOLTAGE = (0, 25) * Volt # wild guess, to spec for 50V NP0 after derating # suggested initial values from AN13219 - self.rx = self.Block(Pn7160RxFilter(resistance=2.2*kOhm(tol=0.05), capacitance=1*nFarad(tol=0.1), - voltage=CAP_VOLTAGE)) + self.rx = self.Block( + Pn7160RxFilter(resistance=2.2 * kOhm(tol=0.05), capacitance=1 * nFarad(tol=0.1), voltage=CAP_VOLTAGE) + ) self.connect(self.ic.rxp, self.rx.out1) self.connect(self.ic.rxn, self.rx.out2) - self.emc = imp.Block(DifferentialLcLowpassFilter( - freq_cutoff=14.7*MHertz, inductance=220*nHenry, input_res=20*Ohm, - freq=SIGNAL_FREQ, current=(0, 300)*mAmp, voltage=CAP_VOLTAGE - )) # TODO should calculate impedance separately from the filter + self.emc = imp.Block( + DifferentialLcLowpassFilter( + freq_cutoff=14.7 * MHertz, + inductance=220 * nHenry, + input_res=20 * Ohm, + freq=SIGNAL_FREQ, + current=(0, 300) * mAmp, + voltage=CAP_VOLTAGE, + ) + ) # TODO should calculate impedance separately from the filter self.connect(self.ic.tx1, self.emc.in1) self.connect(self.ic.tx2, self.emc.in2) # footprint generated from https://github.com/nideri/nfc_antenna_generator # python antGen.py -f ref -n 4 -l 40 -w 40 -c 0.4 -s 0.3 -d 0.3 -t 3 - self.ant = self.Block(NfcAntenna(ant_footprint='board:an13219', - freq=SIGNAL_FREQ, inductance=1522*nHenry, # from NXP AN13219 PCB antenna - resistance=1.40*Ohm, capacitance=2.0*pFarad)) + self.ant = self.Block( + NfcAntenna( + ant_footprint="board:an13219", + freq=SIGNAL_FREQ, + inductance=1522 * nHenry, # from NXP AN13219 PCB antenna + resistance=1.40 * Ohm, + capacitance=2.0 * pFarad, + ) + ) self.damp = self.Block(NfcAntennaDampening(target_q=20, ant_r=self.ant.z_real, ant_x=self.ant.z_imag)) self.connect(self.damp.ant1, self.ant.ant1) self.connect(self.damp.ant2, self.ant.ant2) - self.match = imp.Block(DifferentialLLowPassFilter( # complex conjugate both sides - freq=SIGNAL_FREQ, src_r=self.emc.z_real, src_x=-self.emc.z_imag, - snk_r=self.damp.z_real, snk_x=-self.damp.z_imag, voltage=CAP_VOLTAGE - )) + self.match = imp.Block( + DifferentialLLowPassFilter( # complex conjugate both sides + freq=SIGNAL_FREQ, + src_r=self.emc.z_real, + src_x=-self.emc.z_imag, + snk_r=self.damp.z_real, + snk_x=-self.damp.z_imag, + voltage=CAP_VOLTAGE, + ) + ) self.connect(self.emc.out1, self.match.in1, self.rx.in1) self.connect(self.emc.out2, self.match.in2, self.rx.in2) diff --git a/edg/parts/Rf_Sx1262.py b/edg/parts/Rf_Sx1262.py index 0959289a7..054a060b3 100644 --- a/edg/parts/Rf_Sx1262.py +++ b/edg/parts/Rf_Sx1262.py @@ -21,7 +21,10 @@ def __init__(self) -> None: self.ctrl = self.Port(Passive()) # modeled in container, series resistor recommended self.restricted_availiability = self.Parameter(BoolExpr(True)) - self.require(self.restricted_availiability == False, "part has restricted availiability, DO NOT USE, see class def for details") + self.require( + self.restricted_availiability == False, + "part has restricted availiability, DO NOT USE, see class def for details", + ) # from a major component distributor, the vendor specifically forbids sales to individuals # https://www.reddit.com/r/rfelectronics/comments/1dubvj7/psemi_forbidding_resellers_from_selling_to/ has speculation # note that most other comparable RF switches do not have single-pin control @@ -32,19 +35,21 @@ def contents(self) -> None: super().contents() self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-363_SC-70-6', + "U", + "Package_TO_SOT_SMD:SOT-363_SC-70-6", { - '1': self.rf1, - '2': self.gnd, - '3': self.rf2, - '4': self.ctrl, - '5': self.rfc, - '6': self.vdd, + "1": self.rf1, + "2": self.gnd, + "3": self.rf2, + "4": self.ctrl, + "5": self.rfc, + "6": self.vdd, }, - mfr='pSemi', part='PE4259', - datasheet='https://www.psemi.com/pdf/datasheets/pe4259ds.pdf' + mfr="pSemi", + part="PE4259", + datasheet="https://www.psemi.com/pdf/datasheets/pe4259ds.pdf", ) - self.assign(self.lcsc_part, 'C470892') + self.assign(self.lcsc_part, "C470892") self.assign(self.actual_basic_part, False) @@ -53,6 +58,7 @@ class Pe4259(Nonstrict3v3Compatible, Block): Requires all RF pins be held at 0v or are DC-blocked with a series cap. TODO: perhaps a RfSwitch base class? maybe some relation to AnalogSwitch? (though not valid at DC) """ + def __init__(self) -> None: super().__init__() self.ic = self.Block(Pe4259_Device()) @@ -67,23 +73,29 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() - self.vdd_res = self.Block(Resistor(1*kOhm(tol=0.05))) + self.vdd_res = self.Block(Resistor(1 * kOhm(tol=0.05))) self.connect(self.vdd_res.b, self.ic.vdd) - self.connect(self.vdd_res.a.adapt_to(VoltageSink.from_gnd( - self.gnd, - voltage_limits=self.nonstrict_3v3_compatible.then_else( - (1.8, 4.0)*Volt, - (1.8, 3.3)*Volt), - current_draw=(9, 20)*uAmp, - )), self.vdd) - - self.ctrl_res = self.Block(Resistor(1*kOhm(tol=0.05))) + self.connect( + self.vdd_res.a.adapt_to( + VoltageSink.from_gnd( + self.gnd, + voltage_limits=self.nonstrict_3v3_compatible.then_else((1.8, 4.0) * Volt, (1.8, 3.3) * Volt), + current_draw=(9, 20) * uAmp, + ) + ), + self.vdd, + ) + + self.ctrl_res = self.Block(Resistor(1 * kOhm(tol=0.05))) self.connect(self.ctrl_res.b, self.ic.ctrl) - self.connect(self.ctrl_res.a.adapt_to(DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - input_threshold_factor=(0.3, 0.7) - )), self.ctrl) + self.connect( + self.ctrl_res.a.adapt_to( + DigitalSink.from_supply( + self.gnd, self.vdd, voltage_limit_tolerance=(-0.3, 0.3) * Volt, input_threshold_factor=(0.3, 0.7) + ) + ), + self.ctrl, + ) class Sx1262BalunLike(InternalSubcircuit, GeneratorBlock): @@ -91,6 +103,7 @@ class Sx1262BalunLike(InternalSubcircuit, GeneratorBlock): This consists of a high-pass L impedance-matching network plus a capacitor to balance out the differential input voltages. The series cap then needs to be adjusted for the mismatch from the balancing cap. """ + @classmethod def _calculate_values(cls, freq: float, z_int: complex, z_ext: complex) -> Tuple[float, float, float]: """Calculate component values, returning the inductor, series cap, and parallel cap. @@ -103,9 +116,16 @@ def _calculate_values(cls, freq: float, z_int: complex, z_ext: complex) -> Tuple l_c_new = (l_c * cp) / (cp - l_c) return l_l, l_c_new, cp - def __init__(self, frequency: FloatLike, src_resistance: FloatLike, src_reactance: FloatLike, - load_resistance: FloatLike, tolerance: FloatLike, - voltage: RangeLike, current: RangeLike): + def __init__( + self, + frequency: FloatLike, + src_resistance: FloatLike, + src_reactance: FloatLike, + load_resistance: FloatLike, + tolerance: FloatLike, + voltage: RangeLike, + current: RangeLike, + ): super().__init__() self.gnd = self.Port(Ground.empty(), [Common]) self.input = self.Port(Passive.empty()) @@ -120,8 +140,9 @@ def __init__(self, frequency: FloatLike, src_resistance: FloatLike, src_reactanc self.current = self.ArgParameter(current) self.tolerance = self.ArgParameter(tolerance) - self.generator_param(self.frequency, self.src_resistance, self.src_reactance, self.load_resistance, - self.tolerance) + self.generator_param( + self.frequency, self.src_resistance, self.src_reactance, self.load_resistance, self.tolerance + ) @override def generate(self) -> None: @@ -133,9 +154,9 @@ def generate(self) -> None: l, c, c_p = self._calculate_values(self.get(self.frequency), zs, rl) tolerance = self.get(self.tolerance) - self.l = self.Block(Inductor(inductance=l*Henry(tol=tolerance), current=self.current)) - self.c = self.Block(Capacitor(capacitance=c*Farad(tol=tolerance), voltage=self.voltage)) - self.c_p = self.Block(Capacitor(capacitance=c_p*Farad(tol=tolerance), voltage=self.voltage)) + self.l = self.Block(Inductor(inductance=l * Henry(tol=tolerance), current=self.current)) + self.c = self.Block(Capacitor(capacitance=c * Farad(tol=tolerance), voltage=self.voltage)) + self.c_p = self.Block(Capacitor(capacitance=c_p * Farad(tol=tolerance), voltage=self.voltage)) self.connect(self.input, self.c.pos) self.connect(self.rfi_n, self.c.neg, self.l.a) @@ -147,17 +168,20 @@ class Sx1262_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground(), [Common]) - self.vbat = self.Port(VoltageSink.from_gnd( - self.gnd, # include Vbat - voltage_limits=(1.8, 3.7)*Volt, - current_draw=(0.000160, 118)*mAmp)) # from IDDOFF to max TX - self.vbat_io = self.Port(VoltageSink.from_gnd( - self.gnd, - voltage_limits=(1.8, 3.7)*Volt)) # no separate current draw given, lumped w/ Vbat - - self.vreg = self.Port(VoltageSource( # may be a LDO output or DC-DC input - voltage_out=1.55*Volt(tol=0) # no tolerance specified - )) + self.vbat = self.Port( + VoltageSink.from_gnd( + self.gnd, voltage_limits=(1.8, 3.7) * Volt, current_draw=(0.000160, 118) * mAmp # include Vbat + ) + ) # from IDDOFF to max TX + self.vbat_io = self.Port( + VoltageSink.from_gnd(self.gnd, voltage_limits=(1.8, 3.7) * Volt) + ) # no separate current draw given, lumped w/ Vbat + + self.vreg = self.Port( + VoltageSource( # may be a LDO output or DC-DC input + voltage_out=1.55 * Volt(tol=0) # no tolerance specified + ) + ) self.dcc_sw = self.Port(Passive()) self.xtal = self.Port(CrystalDriver()) # TODO loading caps not needed @@ -165,64 +189,65 @@ def __init__(self) -> None: self.rfi_p = self.Port(Passive()) self.rfi_n = self.Port(Passive()) self.rfo = self.Port(Passive()) - self.vr_pa = self.Port(VoltageSource( - voltage_out=(0, 3.1)*Volt # from power supply scheme figure - )) + self.vr_pa = self.Port(VoltageSource(voltage_out=(0, 3.1) * Volt)) # from power supply scheme figure dio_model = DigitalBidir.from_supply( - self.gnd, self.vbat_io, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - current_limits=(-0, 0)*mAmp, # not specified - input_threshold_factor=(0.3, 0.7) + self.gnd, + self.vbat_io, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + current_limits=(-0, 0) * mAmp, # not specified + input_threshold_factor=(0.3, 0.7), ) self.dio1 = self.Port(dio_model, optional=True) # generic IRQ self.dio2 = self.Port(dio_model, optional=True) # generic IRQ, plus TX switch (1=Tx, 0=otherwise) self.dio3 = self.Port(dio_model, optional=True) # generic IRQ, plus TXCO control - self.spi = self.Port(SpiPeripheral(dio_model, frequency_limit=(0, 16)*MHertz)) + self.spi = self.Port(SpiPeripheral(dio_model, frequency_limit=(0, 16) * MHertz)) self.nss = self.Port(DigitalSink.from_bidir(dio_model)) self.busy = self.Port(DigitalSource.from_bidir(dio_model), optional=True) - self.nreset = self.Port(DigitalSink.from_supply( - self.gnd, self.vbat_io, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - input_threshold_factor=(0.2, 0.7) - )) + self.nreset = self.Port( + DigitalSink.from_supply( + self.gnd, self.vbat_io, voltage_limit_tolerance=(-0.3, 0.3) * Volt, input_threshold_factor=(0.2, 0.7) + ) + ) @override def contents(self) -> None: self.footprint( - 'U', 'Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm', + "U", + "Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm", { - '1': self.vbat, # Vdd_in, internally connected to pin 10 - '2': self.gnd, - '3': self.xtal.xtal_in, # XTA - '4': self.xtal.xtal_out, # XTB - '5': self.gnd, - '6': self.dio3, - '7': self.vreg, - '8': self.gnd, - '9': self.dcc_sw, - '10': self.vbat, - '11': self.vbat_io, - '12': self.dio2, - '13': self.dio1, - '14': self.busy, - '15': self.nreset, - '16': self.spi.miso, - '17': self.spi.mosi, - '18': self.spi.sck, - '19': self.nss, - '20': self.gnd, - '21': self.rfi_p, - '22': self.rfi_n, - '23': self.rfo, - '24': self.vr_pa, - '25': self.gnd, # EP, labeled as pin 0 on datasheet + "1": self.vbat, # Vdd_in, internally connected to pin 10 + "2": self.gnd, + "3": self.xtal.xtal_in, # XTA + "4": self.xtal.xtal_out, # XTB + "5": self.gnd, + "6": self.dio3, + "7": self.vreg, + "8": self.gnd, + "9": self.dcc_sw, + "10": self.vbat, + "11": self.vbat_io, + "12": self.dio2, + "13": self.dio1, + "14": self.busy, + "15": self.nreset, + "16": self.spi.miso, + "17": self.spi.mosi, + "18": self.spi.sck, + "19": self.nss, + "20": self.gnd, + "21": self.rfi_p, + "22": self.rfi_n, + "23": self.rfo, + "24": self.vr_pa, + "25": self.gnd, # EP, labeled as pin 0 on datasheet }, - mfr='Semtech Corporation', part='SX1262IMLTRT', - datasheet='https://semtech.my.salesforce.com/sfc/p/E0000000JelG/a/2R000000Un7F/yT.fKdAr9ZAo3cJLc4F2cBdUsMftpT2vsOICP7NmvMo' + mfr="Semtech Corporation", + part="SX1262IMLTRT", + datasheet="https://semtech.my.salesforce.com/sfc/p/E0000000JelG/a/2R000000Un7F/yT.fKdAr9ZAo3cJLc4F2cBdUsMftpT2vsOICP7NmvMo", ) - self.assign(self.lcsc_part, 'C191341') + self.assign(self.lcsc_part, "C191341") self.assign(self.actual_basic_part, False) @@ -231,6 +256,7 @@ class Sx1262(Resettable, DiscreteRfWarning, Block): Up to 62.5kb/s in LoRa mode and 300kb/s in FSK mode. TODO: RF frequency parameterization """ + def __init__(self) -> None: super().__init__() self.ic = self.Block(Sx1262_Device()) @@ -250,28 +276,32 @@ def contents(self) -> None: super().contents() self.connect(self.ic.vbat_io, self.pwr) - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.xtal = imp.Block(Crystal(32*MHertz(tol=30e-6))) # 30ppm for LoRaWAN systems + with self.implicit_connect(ImplicitConnect(self.gnd, [Common])) as imp: + self.xtal = imp.Block(Crystal(32 * MHertz(tol=30e-6))) # 30ppm for LoRaWAN systems self.connect(self.xtal.crystal, self.ic.xtal) - self.vreg_cap = imp.Block(DecouplingCapacitor(470*nFarad(tol=0.2))).connected(pwr=self.ic.vreg) - self.vbat_cap = imp.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(pwr=self.ic.vbat) - self.vdd_cap = imp.Block(DecouplingCapacitor(1*uFarad(tol=0.2))).connected(pwr=self.ic.vbat) - self.vrpa_cap0 = imp.Block(DecouplingCapacitor(47*pFarad(tol=0.2))).connected(pwr=self.ic.vr_pa) - self.vrpa_cap1 = imp.Block(DecouplingCapacitor(47*nFarad(tol=0.2))).connected(pwr=self.ic.vr_pa) - - self.dcc_l = self.Block(Inductor( # from datasheet 5.1.5 - 15*uHenry(tol=0.2), current=(0, 100)*mAmp, frequency=20*MHertz(tol=0), resistance_dc=(0, 2)*Ohm)) + self.vreg_cap = imp.Block(DecouplingCapacitor(470 * nFarad(tol=0.2))).connected(pwr=self.ic.vreg) + self.vbat_cap = imp.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(pwr=self.ic.vbat) + self.vdd_cap = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))).connected(pwr=self.ic.vbat) + self.vrpa_cap0 = imp.Block(DecouplingCapacitor(47 * pFarad(tol=0.2))).connected(pwr=self.ic.vr_pa) + self.vrpa_cap1 = imp.Block(DecouplingCapacitor(47 * nFarad(tol=0.2))).connected(pwr=self.ic.vr_pa) + + self.dcc_l = self.Block( + Inductor( # from datasheet 5.1.5 + 15 * uHenry(tol=0.2), + current=(0, 100) * mAmp, + frequency=20 * MHertz(tol=0), + resistance_dc=(0, 2) * Ohm, + ) + ) self.connect(self.dcc_l.a, self.ic.dcc_sw) self.connect(self.dcc_l.b.adapt_to(VoltageSink()), self.ic.vreg) # actually the source, but ic assumes ldo # RF signal chain # switch - rf_voltage = (0, 10)*Volt # assumed, wild guess - rf_current = (0, 100)*mAmp # assumed, wild guess - dcblock_model = Capacitor(47*pFarad(tol=0.05), voltage=rf_voltage) + rf_voltage = (0, 10) * Volt # assumed, wild guess + rf_current = (0, 100) * mAmp # assumed, wild guess + dcblock_model = Capacitor(47 * pFarad(tol=0.05), voltage=rf_voltage) self.rf_sw = imp.Block(Pe4259()) self.connect(self.rf_sw.vdd, self.pwr) self.connect(self.rf_sw.ctrl, self.ic.dio2) @@ -281,22 +311,35 @@ def contents(self) -> None: self.connect(self.rfc_dcblock.neg, self.rf_sw.rfc) # transmit filter chain - self.vrpa_choke = self.Block(Inductor(47*nHenry(tol=0.05))) # see ST AN5457 for other frequencies + self.vrpa_choke = self.Block(Inductor(47 * nHenry(tol=0.05))) # see ST AN5457 for other frequencies self.connect(self.vrpa_choke.a.adapt_to(VoltageSink()), self.ic.vr_pa) self.connect(self.ic.rfo, self.vrpa_choke.b) (self.tx_l, self.tx_pi), _ = self.chain( self.ic.rfo, - imp.Block(LLowPassFilterWith2HNotch(915*MHertz, 11.7*Ohm, -4.8*Ohm, 50*Ohm, 0.2, - rf_voltage, rf_current)), - imp.Block(PiLowPassFilter((915-915/2, 915+915/2)*MHertz, 50*Ohm, 0, 50*Ohm, 0.2, # Q=1 - rf_voltage, rf_current)), - self.tx_dcblock.neg + imp.Block( + LLowPassFilterWith2HNotch( + 915 * MHertz, 11.7 * Ohm, -4.8 * Ohm, 50 * Ohm, 0.2, rf_voltage, rf_current + ) + ), + imp.Block( + PiLowPassFilter( + (915 - 915 / 2, 915 + 915 / 2) * MHertz, + 50 * Ohm, + 0, + 50 * Ohm, + 0.2, # Q=1 + rf_voltage, + rf_current, + ) + ), + self.tx_dcblock.neg, ) # receive filter chain - self.balun = imp.Block(Sx1262BalunLike(915*MHertz, 74*Ohm, -134*Ohm, 50*Ohm, 0.15, - rf_voltage, rf_current)) + self.balun = imp.Block( + Sx1262BalunLike(915 * MHertz, 74 * Ohm, -134 * Ohm, 50 * Ohm, 0.15, rf_voltage, rf_current) + ) self.connect(self.balun.input, self.rf_sw.rf2) self.connect(self.balun.rfi_n, self.ic.rfi_n) self.connect(self.balun.rfi_p, self.ic.rfi_p) @@ -304,7 +347,22 @@ def contents(self) -> None: # antenna (self.ant_pi, self.ant), _ = self.chain( self.rfc_dcblock.pos, - imp.Block(PiLowPassFilter((915-915/2, 915+915/2)*MHertz, 50*Ohm, 0, 50*Ohm, 0.2, # Q=1 - rf_voltage, rf_current)), - imp.Block(Antenna(frequency=(915-0.5, 915+0.5)*MHertz, # up to 500kHz bandwidth in LoRa mode - impedance=50*Ohm(tol=0.1), power=(0, 0.159)*Watt))) # +22dBm + imp.Block( + PiLowPassFilter( + (915 - 915 / 2, 915 + 915 / 2) * MHertz, + 50 * Ohm, + 0, + 50 * Ohm, + 0.2, # Q=1 + rf_voltage, + rf_current, + ) + ), + imp.Block( + Antenna( + frequency=(915 - 0.5, 915 + 0.5) * MHertz, # up to 500kHz bandwidth in LoRa mode + impedance=50 * Ohm(tol=0.1), + power=(0, 0.159) * Watt, + ) + ), + ) # +22dBm diff --git a/edg/parts/RotaryEncoder_Alps.py b/edg/parts/RotaryEncoder_Alps.py index bd7212899..2ee3ae1f8 100644 --- a/edg/parts/RotaryEncoder_Alps.py +++ b/edg/parts/RotaryEncoder_Alps.py @@ -5,70 +5,79 @@ class Ec11eWithSwitch(RotaryEncoderSwitch, RotaryEncoder, JlcPart, FootprintBlock): - """Generic EC11E PTH rotary with pushbutton switch. - Default part is EC11E18244A5, with 1.5mm pushbutton travel, 36 detents (finest), - but footprint should be compatible with other parts in the EC11E w/ switch series""" - @override - def contents(self) -> None: - super().contents() + """Generic EC11E PTH rotary with pushbutton switch. + Default part is EC11E18244A5, with 1.5mm pushbutton travel, 36 detents (finest), + but footprint should be compatible with other parts in the EC11E w/ switch series""" - self.footprint( - 'SW', 'Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm', - { - 'A': self.a, - 'B': self.b, - 'C': self.com, - 'S1': self.sw, - 'S2': self.com, - }, - mfr='Alps Alpine', part='EC11E18244A5', - datasheet='https://tech.alpsalpine.com/assets/products/catalog/ec11.en.pdf' - ) - self.assign(self.lcsc_part, 'C255515') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "SW", + "Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm", + { + "A": self.a, + "B": self.b, + "C": self.com, + "S1": self.sw, + "S2": self.com, + }, + mfr="Alps Alpine", + part="EC11E18244A5", + datasheet="https://tech.alpsalpine.com/assets/products/catalog/ec11.en.pdf", + ) + self.assign(self.lcsc_part, "C255515") + self.assign(self.actual_basic_part, False) class Ec11j15WithSwitch(RotaryEncoderSwitch, RotaryEncoder, JlcPart, FootprintBlock): - """Generic EC11J15 SMD rotary with pushbutton switch. - Default part is EC11J1525402, with 1.5mm pushbutton travel, 30 detents (finest), - but footprint should be compatible with other parts in the EC11J15 w/ switch series""" - @override - def contents(self) -> None: - super().contents() + """Generic EC11J15 SMD rotary with pushbutton switch. + Default part is EC11J1525402, with 1.5mm pushbutton travel, 30 detents (finest), + but footprint should be compatible with other parts in the EC11J15 w/ switch series""" + + @override + def contents(self) -> None: + super().contents() - self.footprint( - 'SW', 'edg:RotaryEncoder_Alps_EC11J15-Switch', - { - 'A': self.a, - 'B': self.b, - 'C': self.com, - 'S1': self.sw, - 'S2': self.com, - }, - mfr='Alps Alpine', part='EC11J1525402', - # datasheet / catalog doesn't appear to be available from the manufacturer like the PTH version - datasheet='https://cdn-shop.adafruit.com/product-files/5454/5454_1837001.pdf' - ) - self.assign(self.lcsc_part, 'C209762') - self.assign(self.actual_basic_part, False) + self.footprint( + "SW", + "edg:RotaryEncoder_Alps_EC11J15-Switch", + { + "A": self.a, + "B": self.b, + "C": self.com, + "S1": self.sw, + "S2": self.com, + }, + mfr="Alps Alpine", + part="EC11J1525402", + # datasheet / catalog doesn't appear to be available from the manufacturer like the PTH version + datasheet="https://cdn-shop.adafruit.com/product-files/5454/5454_1837001.pdf", + ) + self.assign(self.lcsc_part, "C209762") + self.assign(self.actual_basic_part, False) class Ec05e(RotaryEncoder, JlcPart, FootprintBlock): - """Generic EC05E PTH rotary encoder with hollow shaft. - Default part is EC05E1220401, horizontal-mount part.""" - @override - def contents(self) -> None: - super().contents() + """Generic EC05E PTH rotary encoder with hollow shaft. + Default part is EC05E1220401, horizontal-mount part.""" + + @override + def contents(self) -> None: + super().contents() - self.footprint( - 'SW', 'edg:RotaryEncoder_Alps_EC05E', - { - 'A': self.a, - 'B': self.b, - 'C': self.com, - }, - mfr='Alps Alpine', part='EC05E1220401', - datasheet='https://tech.alpsalpine.com/e/products/detail/EC05E1220401/' - ) - self.assign(self.lcsc_part, 'C116648') - self.assign(self.actual_basic_part, False) + self.footprint( + "SW", + "edg:RotaryEncoder_Alps_EC05E", + { + "A": self.a, + "B": self.b, + "C": self.com, + }, + mfr="Alps Alpine", + part="EC05E1220401", + datasheet="https://tech.alpsalpine.com/e/products/detail/EC05E1220401/", + ) + self.assign(self.lcsc_part, "C116648") + self.assign(self.actual_basic_part, False) diff --git a/edg/parts/RotaryEncoder_Bourns.py b/edg/parts/RotaryEncoder_Bourns.py index 22df2b9b8..aebb2ac04 100644 --- a/edg/parts/RotaryEncoder_Bourns.py +++ b/edg/parts/RotaryEncoder_Bourns.py @@ -4,22 +4,25 @@ class Pec11s(RotaryEncoderSwitch, RotaryEncoder, FootprintBlock): - """Bourns PEC11S SMD rotary with pushbutton switch. - Default part is PEC11S-929K-S0015, but entire series is footprint-compatible. - While the copper pattern is compatible with the EC11J15, there is a different mounting boss.""" - @override - def contents(self) -> None: - super().contents() + """Bourns PEC11S SMD rotary with pushbutton switch. + Default part is PEC11S-929K-S0015, but entire series is footprint-compatible. + While the copper pattern is compatible with the EC11J15, there is a different mounting boss.""" - self.footprint( - 'SW', 'edg:RotaryEncoder_Bourns_PEC11S', - { - 'A': self.a, - 'B': self.b, - 'C': self.com, - 'S1': self.sw, - 'S2': self.com, - }, - mfr='Bourns', part='PEC11S-929K-S0015', - datasheet='https://www.bourns.com/docs/Product-Datasheets/PEC11S.pdf' - ) + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "SW", + "edg:RotaryEncoder_Bourns_PEC11S", + { + "A": self.a, + "B": self.b, + "C": self.com, + "S1": self.sw, + "S2": self.com, + }, + mfr="Bourns", + part="PEC11S-929K-S0015", + datasheet="https://www.bourns.com/docs/Product-Datasheets/PEC11S.pdf", + ) diff --git a/edg/parts/Rtc_Pcf2129.py b/edg/parts/Rtc_Pcf2129.py index 554c5e19a..f4537c538 100644 --- a/edg/parts/Rtc_Pcf2129.py +++ b/edg/parts/Rtc_Pcf2129.py @@ -4,101 +4,100 @@ class Pcf2129_Device(InternalSubcircuit, FootprintBlock): - """RTC with integrated crystal. SO-16 version""" - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSink( - voltage_limits=(1.8, 4.2) * Volt, - current_draw=(0, 800) * uAmp - ), [Power]) - self.pwr_bat = self.Port(VoltageSink( - voltage_limits=(1.8, 4.2) * Volt, - current_draw=(0, 100) * nAmp - )) - self.gnd = self.Port(Ground(), [Common]) - - dio_model = DigitalBidir( - voltage_limits=(-0.5, self.pwr.link().voltage.lower() + 0.5), - voltage_out=(0, self.pwr.link().voltage.lower()), - current_limits=(-1, 1) * mAmp, # TODO higher sink current on SDA/nCE - input_thresholds=(0.25 * self.pwr.link().voltage.upper(), - 0.7 * self.pwr.link().voltage.upper()), - output_thresholds=(0, self.pwr.link().voltage.upper()), - ) - - self.spi = self.Port(SpiPeripheral(dio_model), [Output]) - self.cs = self.Port(DigitalSink.from_bidir(dio_model)) - - opendrain_model = DigitalSource.low_from_supply(self.gnd, current_limits=(-1, 0)*mAmp) - self.clkout = self.Port(opendrain_model, optional=True) - self.int = self.Port(opendrain_model, optional=True) - - self.bbs = self.Port(VoltageSource( - voltage_out=self.pwr_bat.link().voltage - )) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_SO:SOIC-16W_7.5x10.3mm_P1.27mm', - { - '1': self.spi.sck, # also I2C SCL - '2': self.spi.mosi, - '3': self.spi.miso, - '4': self.cs, # also I2C SDA - '5': self.gnd, # IFS interface select - # '6': , # nTS, active low timestamp input with internal 200k pullup - '7': self.clkout, - '8': self.gnd, - '13': self.int, - '14': self.bbs, # output voltage, battery-backed - '15': self.pwr_bat, - '16': self.pwr, - }, - mfr='NXP', part='PCF2129T', - datasheet='https://www.nxp.com/docs/en/data-sheet/PCF2129.pdf' - ) + """RTC with integrated crystal. SO-16 version""" + + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink(voltage_limits=(1.8, 4.2) * Volt, current_draw=(0, 800) * uAmp), [Power]) + self.pwr_bat = self.Port(VoltageSink(voltage_limits=(1.8, 4.2) * Volt, current_draw=(0, 100) * nAmp)) + self.gnd = self.Port(Ground(), [Common]) + + dio_model = DigitalBidir( + voltage_limits=(-0.5, self.pwr.link().voltage.lower() + 0.5), + voltage_out=(0, self.pwr.link().voltage.lower()), + current_limits=(-1, 1) * mAmp, # TODO higher sink current on SDA/nCE + input_thresholds=(0.25 * self.pwr.link().voltage.upper(), 0.7 * self.pwr.link().voltage.upper()), + output_thresholds=(0, self.pwr.link().voltage.upper()), + ) + + self.spi = self.Port(SpiPeripheral(dio_model), [Output]) + self.cs = self.Port(DigitalSink.from_bidir(dio_model)) + + opendrain_model = DigitalSource.low_from_supply(self.gnd, current_limits=(-1, 0) * mAmp) + self.clkout = self.Port(opendrain_model, optional=True) + self.int = self.Port(opendrain_model, optional=True) + + self.bbs = self.Port(VoltageSource(voltage_out=self.pwr_bat.link().voltage)) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_SO:SOIC-16W_7.5x10.3mm_P1.27mm", + { + "1": self.spi.sck, # also I2C SCL + "2": self.spi.mosi, + "3": self.spi.miso, + "4": self.cs, # also I2C SDA + "5": self.gnd, # IFS interface select + # '6': , # nTS, active low timestamp input with internal 200k pullup + "7": self.clkout, + "8": self.gnd, + "13": self.int, + "14": self.bbs, # output voltage, battery-backed + "15": self.pwr_bat, + "16": self.pwr, + }, + mfr="NXP", + part="PCF2129T", + datasheet="https://www.nxp.com/docs/en/data-sheet/PCF2129.pdf", + ) class Pcf2129(RealtimeClock, Block): - """RTC with integrated crystal. SO-16 version""" - def __init__(self) -> None: - super().__init__() - - self.ic = self.Block(Pcf2129_Device()) - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.pwr_bat = self.Export(self.ic.pwr_bat) - self.gnd = self.Export(self.ic.gnd, [Common]) - - self.spi = self.Export(self.ic.spi) - self.cs = self.Export(self.ic.cs) - self.clkout = self.Export(self.ic.clkout, optional=True) - self.int = self.Export(self.ic.int, optional=True) - - @override - def contents(self) -> None: - super().contents() - - self.vdd_res = self.Block(SeriesPowerResistor( - 330*Ohm(tol=0.05) - )).connected(self.pwr, self.ic.pwr) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.vdd_cap_0 = imp.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(pwr=self.ic.pwr) - self.vdd_cap_1 = imp.Block(DecouplingCapacitor( - capacitance=4.7*uFarad(tol=0.2), # TODO actually 6.8 on the datasheet - )).connected(pwr=self.ic.pwr) - - self.vbat_cap = imp.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), - )).connected(pwr=self.pwr_bat) - - self.bbs_cap = imp.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), # TODO actually 1-100nF - )).connected(pwr=self.ic.bbs) + """RTC with integrated crystal. SO-16 version""" + + def __init__(self) -> None: + super().__init__() + + self.ic = self.Block(Pcf2129_Device()) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.pwr_bat = self.Export(self.ic.pwr_bat) + self.gnd = self.Export(self.ic.gnd, [Common]) + + self.spi = self.Export(self.ic.spi) + self.cs = self.Export(self.ic.cs) + self.clkout = self.Export(self.ic.clkout, optional=True) + self.int = self.Export(self.ic.int, optional=True) + + @override + def contents(self) -> None: + super().contents() + + self.vdd_res = self.Block(SeriesPowerResistor(330 * Ohm(tol=0.05))).connected(self.pwr, self.ic.pwr) + + with self.implicit_connect(ImplicitConnect(self.gnd, [Common])) as imp: + self.vdd_cap_0 = imp.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(pwr=self.ic.pwr) + self.vdd_cap_1 = imp.Block( + DecouplingCapacitor( + capacitance=4.7 * uFarad(tol=0.2), # TODO actually 6.8 on the datasheet + ) + ).connected(pwr=self.ic.pwr) + + self.vbat_cap = imp.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), + ) + ).connected(pwr=self.pwr_bat) + + self.bbs_cap = imp.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), # TODO actually 1-100nF + ) + ).connected(pwr=self.ic.bbs) diff --git a/edg/parts/SdCards.py b/edg/parts/SdCards.py index 48da72e21..7f0aecd02 100644 --- a/edg/parts/SdCards.py +++ b/edg/parts/SdCards.py @@ -6,113 +6,117 @@ @abstract_block class SdCard(Memory): - """Minimum connections for SD card, with IOs definitions set according to SD card spec""" - def __init__(self) -> None: - super().__init__() + """Minimum connections for SD card, with IOs definitions set according to SD card spec""" - # Power characteristics from SD Physical Layer Simplified Spec Ver 6.00: - # Voltage range from Fig 6-5, current limits from VDD_W_CURR_MAX table in 5.3.2 - note some cards may draw less - self.pwr = self.Port(VoltageSink(voltage_limits=(2.7, 4.6) * Volt, current_draw=(0, 200) * mAmp), [Power]) - self.gnd = self.Port(Ground(), [Common]) # TODO need to peg at 0v + def __init__(self) -> None: + super().__init__() - # IO thresholds from NXP AN10911 "SD(HC)-memory card and MMC interface conditioning" - # Many devices also allow a +/-0.3v tolerance above / below Vdd/Vss - dio_model = DigitalBidir.from_supply( - self.gnd, self.pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - input_threshold_factor=(0.25, 0.625) - ) + # Power characteristics from SD Physical Layer Simplified Spec Ver 6.00: + # Voltage range from Fig 6-5, current limits from VDD_W_CURR_MAX table in 5.3.2 - note some cards may draw less + self.pwr = self.Port(VoltageSink(voltage_limits=(2.7, 4.6) * Volt, current_draw=(0, 200) * mAmp), [Power]) + self.gnd = self.Port(Ground(), [Common]) # TODO need to peg at 0v - self.spi = self.Port(SpiPeripheral(dio_model), [InOut]) # TODO does this port directionality make sense? - self.cs = self.Port(DigitalSink.from_bidir(dio_model)) + # IO thresholds from NXP AN10911 "SD(HC)-memory card and MMC interface conditioning" + # Many devices also allow a +/-0.3v tolerance above / below Vdd/Vss + dio_model = DigitalBidir.from_supply( + self.gnd, self.pwr, voltage_limit_tolerance=(-0.3, 0.3) * Volt, input_threshold_factor=(0.25, 0.625) + ) + + self.spi = self.Port(SpiPeripheral(dio_model), [InOut]) # TODO does this port directionality make sense? + self.cs = self.Port(DigitalSink.from_bidir(dio_model)) class SdSocket(SdCard, Connector, FootprintBlock): - """Full-sized SD card socket""" - def __init__(self) -> None: - super().__init__() - - # TODO switch current rating not provided by datasheet, here's some probably sane default - sw_model = DigitalSource.low_from_supply(self.gnd, current_limits=(-20, 0)*mAmp) - self.cd = self.Port(sw_model, optional=True) - self.wp = self.Port(sw_model, optional=True) - - @override - def contents(self) -> None: - super().contents() - # TODO do we need capacitors? - self.footprint( - 'J', 'Connector_Card:SD_Kyocera_145638009511859+', - { - '1': self.cs, - '2': self.spi.mosi, - '3': self.gnd, - '4': self.pwr, - '5': self.spi.sck, - '6': self.gnd, - '7': self.spi.miso, - # '8': , # unused in SPI mode - # '9': , # unused in SPI mode - '10': self.cd, - '11': self.wp, - - 'SH': self.gnd, # shell - }, - mfr='Kyocera', part='145638009511859+', - datasheet='http://global.kyocera.com/prdct/electro/product/pdf/5638.pdf' - ) + """Full-sized SD card socket""" + + def __init__(self) -> None: + super().__init__() + + # TODO switch current rating not provided by datasheet, here's some probably sane default + sw_model = DigitalSource.low_from_supply(self.gnd, current_limits=(-20, 0) * mAmp) + self.cd = self.Port(sw_model, optional=True) + self.wp = self.Port(sw_model, optional=True) + + @override + def contents(self) -> None: + super().contents() + # TODO do we need capacitors? + self.footprint( + "J", + "Connector_Card:SD_Kyocera_145638009511859+", + { + "1": self.cs, + "2": self.spi.mosi, + "3": self.gnd, + "4": self.pwr, + "5": self.spi.sck, + "6": self.gnd, + "7": self.spi.miso, + # '8': , # unused in SPI mode + # '9': , # unused in SPI mode + "10": self.cd, + "11": self.wp, + "SH": self.gnd, # shell + }, + mfr="Kyocera", + part="145638009511859+", + datasheet="http://global.kyocera.com/prdct/electro/product/pdf/5638.pdf", + ) @abstract_block class MicroSdSocket(SdCard): - """MicroSD socket""" + """MicroSD socket""" class Dm3btDsfPejs(MicroSdSocket, Connector, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - # TODO add pull up resistors and capacitors and w/e? - self.footprint( - 'J', 'Connector_Card:microSD_HC_Hirose_DM3BT-DSF-PEJS', - { - # '1': , # unused in SPI mode - '2': self.cs, - '3': self.spi.mosi, - '4': self.pwr, - '5': self.spi.sck, - '6': self.gnd, - '7': self.spi.miso, - # '8': , # unused in SPI mode - - '11': self.gnd, # shell - }, - mfr='Mirose', part='DM3BT-DSF-PEJS', - datasheet='https://www.hirose.com/product/download/?distributor=digikey&type=2d&lang=en&num=DM3BT-DSF-PEJS' - ) + @override + def contents(self) -> None: + super().contents() + # TODO add pull up resistors and capacitors and w/e? + self.footprint( + "J", + "Connector_Card:microSD_HC_Hirose_DM3BT-DSF-PEJS", + { + # '1': , # unused in SPI mode + "2": self.cs, + "3": self.spi.mosi, + "4": self.pwr, + "5": self.spi.sck, + "6": self.gnd, + "7": self.spi.miso, + # '8': , # unused in SPI mode + "11": self.gnd, # shell + }, + mfr="Mirose", + part="DM3BT-DSF-PEJS", + datasheet="https://www.hirose.com/product/download/?distributor=digikey&type=2d&lang=en&num=DM3BT-DSF-PEJS", + ) class Molex1040310811(MicroSdSocket, Connector, JlcPart, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - # TODO add pull up resistors and capacitors and w/e? - self.footprint( - 'J', 'Connector_Card:microSD_HC_Molex_104031-0811', - { - # '1': , # unused in SPI mode - '2': self.cs, - '3': self.spi.mosi, - '4': self.pwr, - '5': self.spi.sck, - '6': self.gnd, - '7': self.spi.miso, - # '8': , # unused in SPI mode - # '9', '10', card detect switch, unused - '11': self.gnd, # shell - }, - mfr='Molex', part='104031-0811', - datasheet='https://www.molex.com/en-us/products/part-detail/1040310811?display=pdf' - ) - self.assign(self.lcsc_part, 'C585350') - self.assign(self.actual_basic_part, False) + @override + def contents(self) -> None: + super().contents() + # TODO add pull up resistors and capacitors and w/e? + self.footprint( + "J", + "Connector_Card:microSD_HC_Molex_104031-0811", + { + # '1': , # unused in SPI mode + "2": self.cs, + "3": self.spi.mosi, + "4": self.pwr, + "5": self.spi.sck, + "6": self.gnd, + "7": self.spi.miso, + # '8': , # unused in SPI mode + # '9', '10', card detect switch, unused + "11": self.gnd, # shell + }, + mfr="Molex", + part="104031-0811", + datasheet="https://www.molex.com/en-us/products/part-detail/1040310811?display=pdf", + ) + self.assign(self.lcsc_part, "C585350") + self.assign(self.actual_basic_part, False) diff --git a/edg/parts/SolidStateRelay_G3VM_61GR2.py b/edg/parts/SolidStateRelay_G3VM_61GR2.py index d7c836266..eb4de549e 100644 --- a/edg/parts/SolidStateRelay_G3VM_61GR2.py +++ b/edg/parts/SolidStateRelay_G3VM_61GR2.py @@ -4,24 +4,26 @@ class G3VM_61GR2(SolidStateRelay, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.assign(self.led_forward_voltage, (1.18, 1.48)*Volt) - self.assign(self.led_current_limit, (3, 30)*mAmp) - self.assign(self.led_current_recommendation, (5, 25)*mAmp) # typ=10mA - self.assign(self.load_voltage_limit, (-48, 48)*Volt) - self.assign(self.load_current_limit, (-1.3, 1.3)*Amp) - self.assign(self.load_resistance, (80, 130)*mOhm) # 80 mOhm is typical + @override + def contents(self) -> None: + super().contents() + self.assign(self.led_forward_voltage, (1.18, 1.48) * Volt) + self.assign(self.led_current_limit, (3, 30) * mAmp) + self.assign(self.led_current_recommendation, (5, 25) * mAmp) # typ=10mA + self.assign(self.load_voltage_limit, (-48, 48) * Volt) + self.assign(self.load_current_limit, (-1.3, 1.3) * Amp) + self.assign(self.load_resistance, (80, 130) * mOhm) # 80 mOhm is typical - self.footprint( - 'U', 'Package_SO:SO-4_4.4x4.3mm_P2.54mm', - { - '1': self.leda, - '2': self.ledk, - '3': self.feta, - '4': self.fetb, - }, - mfr='Omron Electronics', part='G3VM-61GR2', - datasheet='https://omronfs.omron.com/en_US/ecb/products/pdf/en-g3vm_61gr2.pdf' - ) + self.footprint( + "U", + "Package_SO:SO-4_4.4x4.3mm_P2.54mm", + { + "1": self.leda, + "2": self.ledk, + "3": self.feta, + "4": self.fetb, + }, + mfr="Omron Electronics", + part="G3VM-61GR2", + datasheet="https://omronfs.omron.com/en_US/ecb/products/pdf/en-g3vm_61gr2.pdf", + ) diff --git a/edg/parts/SolidStateRelay_Toshiba.py b/edg/parts/SolidStateRelay_Toshiba.py index 9dab1ffc8..8c1a4b049 100644 --- a/edg/parts/SolidStateRelay_Toshiba.py +++ b/edg/parts/SolidStateRelay_Toshiba.py @@ -4,50 +4,54 @@ class Tlp3545a(SolidStateRelay, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.assign(self.led_forward_voltage, (1.50, 1.80)*Volt) - self.assign(self.led_current_limit, (5, 30)*mAmp) - self.assign(self.led_current_recommendation, (5, 25)*mAmp) # typ=10mA - self.assign(self.load_voltage_limit, (-48, 48)*Volt) # recommended, up to 60 max - self.assign(self.load_current_limit, (-4, 4)*Amp) - self.assign(self.load_resistance, (35, 60)*mOhm) # 'A' connection, supports AC but higher resistance + @override + def contents(self) -> None: + super().contents() + self.assign(self.led_forward_voltage, (1.50, 1.80) * Volt) + self.assign(self.led_current_limit, (5, 30) * mAmp) + self.assign(self.led_current_recommendation, (5, 25) * mAmp) # typ=10mA + self.assign(self.load_voltage_limit, (-48, 48) * Volt) # recommended, up to 60 max + self.assign(self.load_current_limit, (-4, 4) * Amp) + self.assign(self.load_resistance, (35, 60) * mOhm) # 'A' connection, supports AC but higher resistance - self.footprint( - 'U', 'Package_DIP:SMDIP-6_W7.62mm', - { - '1': self.leda, - '2': self.ledk, - # '3': nc, - '4': self.feta, # 'A' connection - # '5': source - common - '6': self.fetb, - }, - mfr='Toshiba', part='TLP3545A(TP1,F', - datasheet='https://toshiba.semicon-storage.com/info/docget.jsp?did=60318&prodName=TLP3545A' - ) + self.footprint( + "U", + "Package_DIP:SMDIP-6_W7.62mm", + { + "1": self.leda, + "2": self.ledk, + # '3': nc, + "4": self.feta, # 'A' connection + # '5': source - common + "6": self.fetb, + }, + mfr="Toshiba", + part="TLP3545A(TP1,F", + datasheet="https://toshiba.semicon-storage.com/info/docget.jsp?did=60318&prodName=TLP3545A", + ) class Tlp170am(SolidStateRelay, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - self.assign(self.led_forward_voltage, (1.1, 1.4)*Volt) # 1.27 nominal - self.assign(self.led_current_limit, (2, 30)*mAmp) - self.assign(self.led_current_recommendation, (2, 25)*mAmp) # typ=2mA - self.assign(self.load_voltage_limit, (-48, 48)*Volt) # recommended, up to 60 max - self.assign(self.load_current_limit, (-700, 700)*mAmp) - self.assign(self.load_resistance, (0.15, 0.30)*Ohm) + @override + def contents(self) -> None: + super().contents() + self.assign(self.led_forward_voltage, (1.1, 1.4) * Volt) # 1.27 nominal + self.assign(self.led_current_limit, (2, 30) * mAmp) + self.assign(self.led_current_recommendation, (2, 25) * mAmp) # typ=2mA + self.assign(self.load_voltage_limit, (-48, 48) * Volt) # recommended, up to 60 max + self.assign(self.load_current_limit, (-700, 700) * mAmp) + self.assign(self.load_resistance, (0.15, 0.30) * Ohm) - self.footprint( - 'U', 'Package_SO:SO-4_4.4x3.6mm_P2.54mm', # package outline by just a tad (0.15mm) - { - '1': self.leda, - '2': self.ledk, - '3': self.feta, - '4': self.fetb, - }, - mfr='Toshiba', part='TLP170AM(TPL,E', - datasheet='https://toshiba.semicon-storage.com/info/TLP170AM_datasheet_en_20210524.pdf?did=69016&prodName=TLP170AM' - ) + self.footprint( + "U", + "Package_SO:SO-4_4.4x3.6mm_P2.54mm", # package outline by just a tad (0.15mm) + { + "1": self.leda, + "2": self.ledk, + "3": self.feta, + "4": self.fetb, + }, + mfr="Toshiba", + part="TLP170AM(TPL,E", + datasheet="https://toshiba.semicon-storage.com/info/TLP170AM_datasheet_en_20210524.pdf?did=69016&prodName=TLP170AM", + ) diff --git a/edg/parts/SpeakerDriver_Analog.py b/edg/parts/SpeakerDriver_Analog.py index 82a2d4f47..cfbdead4b 100644 --- a/edg/parts/SpeakerDriver_Analog.py +++ b/edg/parts/SpeakerDriver_Analog.py @@ -8,10 +8,14 @@ class Lm4871_Device(InternalSubcircuit, FootprintBlock): def __init__(self) -> None: super().__init__() - self.pwr = self.Port(VoltageSink( - voltage_limits=(2.0, 5.5) * Volt, - current_draw=(6.5, 10 + 433) * mAmp, # TODO better estimate of speaker current than 1.5W into 8-ohm load - ), [Power]) + self.pwr = self.Port( + VoltageSink( + voltage_limits=(2.0, 5.5) * Volt, + current_draw=(6.5, 10 + 433) + * mAmp, # TODO better estimate of speaker current than 1.5W into 8-ohm load + ), + [Power], + ) self.gnd = self.Port(Ground(), [Common]) self.inp = self.Port(Passive()) # TODO these aren't actually documented w/ specs =( @@ -26,19 +30,21 @@ def __init__(self) -> None: @override def contents(self) -> None: self.footprint( - 'U', 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', + "U", + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", { - '1': self.gnd, # shutdown # TODO make this a controllable digital pin - '2': self.byp, # bypass - '3': self.inp, # Vin+ - '4': self.inm, # Vin- - '5': self.vo1, - '6': self.pwr, - '7': self.gnd, - '8': self.vo2, + "1": self.gnd, # shutdown # TODO make this a controllable digital pin + "2": self.byp, # bypass + "3": self.inp, # Vin+ + "4": self.inm, # Vin- + "5": self.vo1, + "6": self.pwr, + "7": self.gnd, + "8": self.vo2, }, - mfr='Texas Instruments', part='LM4871MX', - datasheet='https://www.ti.com/lit/ds/symlink/lm4871.pdf' + mfr="Texas Instruments", + part="LM4871MX", + datasheet="https://www.ti.com/lit/ds/symlink/lm4871.pdf", ) @@ -59,26 +65,27 @@ def contents(self) -> None: super().contents() # TODO size component based on higher level input? - self.in_cap = self.Block(DecouplingCapacitor( - capacitance=1.0*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) - - self.byp_cap = self.Block(Capacitor( # TODO bypass should be a pseudo source pin, this can be a DecouplingCap - capacitance=1.0*uFarad(tol=0.2), - voltage=self.pwr.link().voltage # TODO actually half the voltage, but needs const prop - )) + self.in_cap = self.Block( + DecouplingCapacitor( + capacitance=1.0 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) + + self.byp_cap = self.Block( + Capacitor( # TODO bypass should be a pseudo source pin, this can be a DecouplingCap + capacitance=1.0 * uFarad(tol=0.2), + voltage=self.pwr.link().voltage, # TODO actually half the voltage, but needs const prop + ) + ) self.connect(self.gnd, self.byp_cap.neg.adapt_to(Ground())) - self.sig_cap = self.Block(Capacitor( # TODO replace with dc-block filter - capacitance=0.47*uFarad(tol=0.2), - voltage=self.sig.link().voltage - )) - self.sig_res = self.Block(Resistor( - resistance=20*kOhm(tol=0.2) - )) - self.fb_res = self.Block(Resistor( - resistance=20*kOhm(tol=0.2) - )) + self.sig_cap = self.Block( + Capacitor( # TODO replace with dc-block filter + capacitance=0.47 * uFarad(tol=0.2), voltage=self.sig.link().voltage + ) + ) + self.sig_res = self.Block(Resistor(resistance=20 * kOhm(tol=0.2))) + self.fb_res = self.Block(Resistor(resistance=20 * kOhm(tol=0.2))) self.connect(self.sig, self.sig_cap.neg.adapt_to(AnalogSink())) self.connect(self.sig_cap.pos, self.sig_res.a) self.connect(self.sig_res.b, self.fb_res.a, self.ic.inm) @@ -92,16 +99,22 @@ class Tpa2005d1_Device(InternalSubcircuit, JlcPart, FootprintBlock): def __init__(self) -> None: super().__init__() - self.pwr = self.Port(VoltageSink( - voltage_limits=(2.5, 5.5) * Volt, - current_draw=(2.2, 260) * mAmp, # quiescent current to maximum supply current (at 1.1W) in Figure 6 - ), [Power]) + self.pwr = self.Port( + VoltageSink( + voltage_limits=(2.5, 5.5) * Volt, + current_draw=(2.2, 260) * mAmp, # quiescent current to maximum supply current (at 1.1W) in Figure 6 + ), + [Power], + ) self.gnd = self.Port(Ground(), [Common]) - input_port = AnalogSink.from_supply(self.gnd, self.pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - signal_limit_bound=(0.5*Volt, -0.8*Volt), - impedance=(142, 158)*kOhm) + input_port = AnalogSink.from_supply( + self.gnd, + self.pwr, + voltage_limit_tolerance=(-0.3, 0.3) * Volt, + signal_limit_bound=(0.5 * Volt, -0.8 * Volt), + impedance=(142, 158) * kOhm, + ) self.inp = self.Port(input_port) self.inn = self.Port(input_port) @@ -114,28 +127,31 @@ def __init__(self) -> None: @override def contents(self) -> None: self.footprint( - 'U', 'Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.68x1.88mm_ThermalVias', + "U", + "Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.68x1.88mm_ThermalVias", { - '7': self.gnd, - '4': self.inp, # Vin+ - '3': self.inn, # Vin- + "7": self.gnd, + "4": self.inp, # Vin+ + "3": self.inn, # Vin- # pin 2 is NC - '1': self.pwr, # /SHDN - '9': self.gnd, # exposed pad, "must be soldered to a grounded pad" - '6': self.pwr, - '8': self.vo1, - '5': self.vo2, + "1": self.pwr, # /SHDN + "9": self.gnd, # exposed pad, "must be soldered to a grounded pad" + "6": self.pwr, + "8": self.vo1, + "5": self.vo2, }, - mfr='Texas Instruments', part='TPA2005D1', - datasheet='https://www.ti.com/lit/ds/symlink/tpa2005d1.pdf' + mfr="Texas Instruments", + part="TPA2005D1", + datasheet="https://www.ti.com/lit/ds/symlink/tpa2005d1.pdf", ) - self.assign(self.lcsc_part, 'C27396') + self.assign(self.lcsc_part, "C27396") self.assign(self.actual_basic_part, False) class Tpa2005d1(SpeakerDriver, Block): """TPA2005D1 configured in single-ended input mode. Possible semi-pin-compatible with PAM8302AASCR (C113367), but which has internal resistor.""" + def __init__(self, gain: RangeLike = Range.from_tolerance(20, 0.2)): super().__init__() # TODO should be a SpeakerDriver abstract part @@ -152,36 +168,49 @@ def __init__(self, gain: RangeLike = Range.from_tolerance(20, 0.2)): @override def contents(self) -> None: import math + super().contents() - self.pwr_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2), # recommended Vcc cap per 11.1 - )).connected(self.gnd, self.pwr) - self.bulk_cap = self.Block(DecouplingCapacitor( - capacitance=(2.2*0.8, 10*1.2)*uFarad, - )).connected(self.gnd, self.pwr) # "charge reservoir" recommended cap per 11.1, 2.2-10uF (+20% tolerance) + self.pwr_cap = self.Block( + DecouplingCapacitor( + capacitance=0.1 * uFarad(tol=0.2), # recommended Vcc cap per 11.1 + ) + ).connected(self.gnd, self.pwr) + self.bulk_cap = self.Block( + DecouplingCapacitor( + capacitance=(2.2 * 0.8, 10 * 1.2) * uFarad, + ) + ).connected( + self.gnd, self.pwr + ) # "charge reservoir" recommended cap per 11.1, 2.2-10uF (+20% tolerance) # Note, gain = 2 * (142k to 158k)/Ri, recommended gain < 20V/V res_value = (1 / self.gain).shrink_multiply(2 * Range(142e3, 158e3)) in_res_model = Resistor(res_value) - fc = (1, 20)*Hertz # for highpass filter, arbitrary, 20Hz right on the edge of audio frequency + fc = (1, 20) * Hertz # for highpass filter, arbitrary, 20Hz right on the edge of audio frequency self.inp_res = self.Block(in_res_model) - self.inp_cap = self.Block(Capacitor( - capacitance=(1 / (2 * math.pi * fc)).shrink_multiply(1 / self.inp_res.actual_resistance) - .intersect((1*0.8, float('inf'))*uFarad), - voltage=self.sig.link().voltage - )) + self.inp_cap = self.Block( + Capacitor( + capacitance=(1 / (2 * math.pi * fc)) + .shrink_multiply(1 / self.inp_res.actual_resistance) + .intersect((1 * 0.8, float("inf")) * uFarad), + voltage=self.sig.link().voltage, + ) + ) self.connect(self.sig, self.inp_cap.neg.adapt_to(AnalogSink())) self.connect(self.inp_cap.pos, self.inp_res.a) self.connect(self.inp_res.b.adapt_to(AnalogSource()), self.ic.inp) self.inn_res = self.Block(in_res_model) - self.inn_cap = self.Block(Capacitor( - capacitance=(1 / (2 * math.pi * fc)).shrink_multiply(1 / self.inn_res.actual_resistance) - .intersect((1*0.8, float('inf'))*uFarad), - voltage=self.sig.link().voltage - )) + self.inn_cap = self.Block( + Capacitor( + capacitance=(1 / (2 * math.pi * fc)) + .shrink_multiply(1 / self.inn_res.actual_resistance) + .intersect((1 * 0.8, float("inf")) * uFarad), + voltage=self.sig.link().voltage, + ) + ) self.connect(self.gnd, self.inn_cap.neg.adapt_to(Ground())) self.connect(self.inn_cap.pos, self.inn_res.a) self.connect(self.inn_res.b.adapt_to(AnalogSource()), self.ic.inn) @@ -194,15 +223,18 @@ class Pam8302a_Device(InternalSubcircuit, JlcPart, FootprintBlock): def __init__(self) -> None: super().__init__() - self.pwr = self.Port(VoltageSink( - voltage_limits=(2.0, 5.5) * Volt, - current_draw=(0.001, 600) * mAmp, # shutdown current to maximum supply current in typ performance chart - ), [Power]) + self.pwr = self.Port( + VoltageSink( + voltage_limits=(2.0, 5.5) * Volt, + current_draw=(0.001, 600) * mAmp, # shutdown current to maximum supply current in typ performance chart + ), + [Power], + ) self.gnd = self.Port(Ground(), [Common]) - input_port = AnalogSink.from_supply(self.gnd, self.pwr, - voltage_limit_tolerance=(-0.3, 0.3)*Volt, - impedance=(142, 158)*kOhm) + input_port = AnalogSink.from_supply( + self.gnd, self.pwr, voltage_limit_tolerance=(-0.3, 0.3) * Volt, impedance=(142, 158) * kOhm + ) self.inp = self.Port(input_port) self.inn = self.Port(input_port) @@ -215,26 +247,29 @@ def __init__(self) -> None: @override def contents(self) -> None: self.footprint( - 'U', 'Package_SO:MSOP-8_3x3mm_P0.65mm', + "U", + "Package_SO:MSOP-8_3x3mm_P0.65mm", { - '1': self.pwr, # /SHDN + "1": self.pwr, # /SHDN # pin 2 is NC - '3': self.inp, - '4': self.inn, - '5': self.vop, - '6': self.pwr, - '7': self.gnd, - '8': self.von, + "3": self.inp, + "4": self.inn, + "5": self.vop, + "6": self.pwr, + "7": self.gnd, + "8": self.von, }, - mfr='Diodes Incorporated', part='PAM8302AASCR', - datasheet='https://www.diodes.com/assets/Datasheets/PAM8302A.pdf' + mfr="Diodes Incorporated", + part="PAM8302AASCR", + datasheet="https://www.diodes.com/assets/Datasheets/PAM8302A.pdf", ) - self.assign(self.lcsc_part, 'C113367') + self.assign(self.lcsc_part, "C113367") self.assign(self.actual_basic_part, False) class Pam8302a(SpeakerDriver, Block): """PAM8302A configured in single-ended input mode.""" + def __init__(self) -> None: super().__init__() @@ -249,17 +284,18 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() - self.pwr_cap0 = self.Block(DecouplingCapacitor( - capacitance=1*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) - self.pwr_cap1 = self.Block(DecouplingCapacitor( - capacitance=10*uFarad(tol=0.2), - )).connected(self.gnd, self.pwr) - - in_cap_model = Capacitor( - capacitance=0.1*uFarad(tol=0.2), - voltage=self.sig.link().voltage - ) + self.pwr_cap0 = self.Block( + DecouplingCapacitor( + capacitance=1 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) + self.pwr_cap1 = self.Block( + DecouplingCapacitor( + capacitance=10 * uFarad(tol=0.2), + ) + ).connected(self.gnd, self.pwr) + + in_cap_model = Capacitor(capacitance=0.1 * uFarad(tol=0.2), voltage=self.sig.link().voltage) self.inp_cap = self.Block(in_cap_model) self.connect(self.sig, self.inp_cap.neg.adapt_to(AnalogSink())) self.connect(self.inp_cap.pos.adapt_to(AnalogSource()), self.ic.inp) diff --git a/edg/parts/SpeakerDriver_Max98357a.py b/edg/parts/SpeakerDriver_Max98357a.py index c49aec81a..1990323ca 100644 --- a/edg/parts/SpeakerDriver_Max98357a.py +++ b/edg/parts/SpeakerDriver_Max98357a.py @@ -10,15 +10,19 @@ class Max98357a_Device(InternalSubcircuit, JlcPart, SelectorFootprint, PartsTabl def __init__(self) -> None: super().__init__() - self.vdd = self.Port(VoltageSink( - voltage_limits=(2.5, 5.5) * Volt, - current_draw=(0.0006, 3.35 + (3.2/5/0.92)*1000) * mAmp, # shutdown to maximum (3.2W out @ 5V, 92% efficient) - ), [Power]) + self.vdd = self.Port( + VoltageSink( + voltage_limits=(2.5, 5.5) * Volt, + current_draw=(0.0006, 3.35 + (3.2 / 5 / 0.92) * 1000) + * mAmp, # shutdown to maximum (3.2W out @ 5V, 92% efficient) + ), + [Power], + ) self.gnd = self.Port(Ground(), [Common]) din_model = DigitalSink( - voltage_limits=(-0.3, 6)*Volt, # abs max ratings - input_thresholds=(0.6, 0.6)*Volt # only input low voltage given + voltage_limits=(-0.3, 6) * Volt, # abs max ratings + input_thresholds=(0.6, 0.6) * Volt, # only input low voltage given ) self.i2s = self.Port(I2sTargetReceiver(din_model)) @@ -29,55 +33,64 @@ def __init__(self) -> None: @override def generate(self) -> None: super().generate() - if not self.get(self.footprint_spec) or \ - self.get(self.footprint_spec) == 'Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm': - footprint = 'Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm' + if ( + not self.get(self.footprint_spec) + or self.get(self.footprint_spec) == "Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm" + ): + footprint = "Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm" pinning: Dict[str, CircuitPort] = { - '4': self.vdd, # hard tied to left mode only TODO selectable SD_MODE - '7': self.vdd, - '8': self.vdd, - '9': self.out.a, # outp - '1': self.i2s.sd, + "4": self.vdd, # hard tied to left mode only TODO selectable SD_MODE + "7": self.vdd, + "8": self.vdd, + "9": self.out.a, # outp + "1": self.i2s.sd, # '2': gain_slot, # TODO: configurable gain, open = 9dB - '10': self.out.b, # outn - '16': self.i2s.sck, - '3': self.gnd, - '11': self.gnd, - '15': self.gnd, - '14': self.i2s.ws, - '17': self.gnd, # EP, optionally grounded for thermal dissipation + "10": self.out.b, # outn + "16": self.i2s.sck, + "3": self.gnd, + "11": self.gnd, + "15": self.gnd, + "14": self.i2s.ws, + "17": self.gnd, # EP, optionally grounded for thermal dissipation } - part = 'MAX98357AETE+T' - jlc_part = 'C910544' - elif self.get(self.footprint_spec) == 'Package_BGA:Maxim_WLP-9_1.595x1.415_Layout3x3_P0.4mm_Ball0.27mm_Pad0.25mm_NSMD': - footprint = 'Package_BGA:Maxim_WLP-9_1.595x1.415_Layout3x3_P0.4mm_Ball0.27mm_Pad0.25mm_NSMD' + part = "MAX98357AETE+T" + jlc_part = "C910544" + elif ( + self.get(self.footprint_spec) + == "Package_BGA:Maxim_WLP-9_1.595x1.415_Layout3x3_P0.4mm_Ball0.27mm_Pad0.25mm_NSMD" + ): + footprint = "Package_BGA:Maxim_WLP-9_1.595x1.415_Layout3x3_P0.4mm_Ball0.27mm_Pad0.25mm_NSMD" pinning = { - 'A1': self.vdd, # hard tied to left mode only TODO selectable SD_MODE - 'A2': self.vdd, - 'A3': self.out.a, # outp - 'B1': self.i2s.sd, - # 'B2': gain_slot, # TODO: configurable gain, open = 9dB - 'B3': self.out.b, # outn - 'C1': self.i2s.sck, - 'C2': self.gnd, - 'C3': self.i2s.ws, - } - part = 'MAX98357AEWL+T' - jlc_part = 'C2682619' + "A1": self.vdd, # hard tied to left mode only TODO selectable SD_MODE + "A2": self.vdd, + "A3": self.out.a, # outp + "B1": self.i2s.sd, + # 'B2': gain_slot, # TODO: configurable gain, open = 9dB + "B3": self.out.b, # outn + "C1": self.i2s.sck, + "C2": self.gnd, + "C3": self.i2s.ws, + } + part = "MAX98357AEWL+T" + jlc_part = "C2682619" else: raise ValueError() self.footprint( - 'U', footprint, + "U", + footprint, pinning, - mfr='Maxim Integrated', part=part, - datasheet='https://www.analog.com/media/en/technical-documentation/data-sheets/MAX98357A-MAX98357B.pdf' + mfr="Maxim Integrated", + part=part, + datasheet="https://www.analog.com/media/en/technical-documentation/data-sheets/MAX98357A-MAX98357B.pdf", ) self.assign(self.lcsc_part, jlc_part) self.assign(self.actual_basic_part, False) + class Max98357a(SpeakerDriver, Block): """MAX98357A I2S speaker driver with default gain.""" + def __init__(self) -> None: super().__init__() @@ -92,9 +105,6 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]) - ) as imp: - self.pwr_cap0 = imp.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))) - self.pwr_cap1 = imp.Block(DecouplingCapacitor(10*uFarad(tol=0.2))) + with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp: + self.pwr_cap0 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.pwr_cap1 = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) diff --git a/edg/parts/Speakers.py b/edg/parts/Speakers.py index 2d7ee13bf..5b27312bc 100644 --- a/edg/parts/Speakers.py +++ b/edg/parts/Speakers.py @@ -4,22 +4,20 @@ @abstract_block class Speaker(HumanInterface): - """Abstract speaker part with speaker input port.""" - def __init__(self) -> None: - super().__init__() - self.input = self.Port(SpeakerPort.empty(), [Input]) + """Abstract speaker part with speaker input port.""" + + def __init__(self) -> None: + super().__init__() + self.input = self.Port(SpeakerPort.empty(), [Input]) class ConnectorSpeaker(Speaker): - """Speaker that delegates to a PassiveConnector and with configurable impedance.""" - def __init__(self, impedance: RangeLike = 8*Ohm(tol=0)): - super().__init__() - - self.conn = self.Block(PassiveConnector()) - - self.connect(self.input.a, self.conn.pins.request('1').adapt_to(AnalogSink( - impedance=impedance) - )) - self.connect(self.input.b, self.conn.pins.request('2').adapt_to(AnalogSink( - impedance=impedance) - )) + """Speaker that delegates to a PassiveConnector and with configurable impedance.""" + + def __init__(self, impedance: RangeLike = 8 * Ohm(tol=0)): + super().__init__() + + self.conn = self.Block(PassiveConnector()) + + self.connect(self.input.a, self.conn.pins.request("1").adapt_to(AnalogSink(impedance=impedance))) + self.connect(self.input.b, self.conn.pins.request("2").adapt_to(AnalogSink(impedance=impedance))) diff --git a/edg/parts/SpiMemory_93Lc.py b/edg/parts/SpiMemory_93Lc.py index 539c06503..cba97b75e 100644 --- a/edg/parts/SpiMemory_93Lc.py +++ b/edg/parts/SpiMemory_93Lc.py @@ -5,83 +5,93 @@ class E93Lc_B_Device(InternalSubcircuit, GeneratorBlock, JlcPart, FootprintBlock): - PARTS = [ - # 93LC56B seems to be the most popular version - (2*1024, '93LC56B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/21794G.pdf', - 'C190271', False), # BT-I/OT variant - # rest are out of stock at JLC in OT package - # TODO check datasheets for the rest for parameter accuracy - # (1*1024, '93LC46B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/20001749K.pdf', - # 'C2061604', False), # BT-E/OT variant - # (4*1024, '93LC66B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/21795E.pdf', - # 'C2061454', False), # BT-I/OT variant - # (8*1024, '93LC76B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/21796M.pdf', - # 'C2063754', False), # BT-E/OT variant - # (16*1024, '93LC86B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/21797L.pdf', - # 'C616337', False), # BT-I/OT variant - ] + PARTS = [ + # 93LC56B seems to be the most popular version + ( + 2 * 1024, + "93LC56B", + "https://ww1.microchip.com/downloads/en/DeviceDoc/21794G.pdf", + "C190271", + False, + ), # BT-I/OT variant + # rest are out of stock at JLC in OT package + # TODO check datasheets for the rest for parameter accuracy + # (1*1024, '93LC46B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/20001749K.pdf', + # 'C2061604', False), # BT-E/OT variant + # (4*1024, '93LC66B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/21795E.pdf', + # 'C2061454', False), # BT-I/OT variant + # (8*1024, '93LC76B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/21796M.pdf', + # 'C2063754', False), # BT-E/OT variant + # (16*1024, '93LC86B', 'https://ww1.microchip.com/downloads/en/DeviceDoc/21797L.pdf', + # 'C616337', False), # BT-I/OT variant + ] - def __init__(self, size: RangeLike): - super().__init__() - self.vcc = self.Port(VoltageSink( - voltage_limits=(1.5, 5.5)*Volt, # Table 1-1 VPOR to Vcc max under I test conditions - current_draw=(0.001, 2)*mAmp # standby max to write max - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) + def __init__(self, size: RangeLike): + super().__init__() + self.vcc = self.Port( + VoltageSink( + voltage_limits=(1.5, 5.5) * Volt, # Table 1-1 VPOR to Vcc max under I test conditions + current_draw=(0.001, 2) * mAmp, # standby max to write max + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) - dio_model = DigitalBidir.from_supply( - self.gnd, self.vcc, - voltage_limit_tolerance=(-0.6, 1), - input_threshold_abs=(0.8, 2.0) # Table 1-1, for Vcc > 2.7 - ) - self.spi = self.Port(SpiPeripheral(dio_model, (0, 2) * MHertz)) # for Vcc >= 2.5 - self.cs = self.Port(dio_model) + dio_model = DigitalBidir.from_supply( + self.gnd, + self.vcc, + voltage_limit_tolerance=(-0.6, 1), + input_threshold_abs=(0.8, 2.0), # Table 1-1, for Vcc > 2.7 + ) + self.spi = self.Port(SpiPeripheral(dio_model, (0, 2) * MHertz)) # for Vcc >= 2.5 + self.cs = self.Port(dio_model) - self.actual_size = self.Parameter(IntExpr()) + self.actual_size = self.Parameter(IntExpr()) - self.size = self.ArgParameter(size) - self.generator_param(self.size) + self.size = self.ArgParameter(size) + self.generator_param(self.size) - @override - def generate(self) -> None: - super().generate() - suitable_parts = [part for part in self.PARTS if part[0] in self.get(self.size)] - assert suitable_parts, "no memory in requested size range" - part_size, part_pn, part_datasheet, part_lcsc, part_lcsc_basic = suitable_parts[0] + @override + def generate(self) -> None: + super().generate() + suitable_parts = [part for part in self.PARTS if part[0] in self.get(self.size)] + assert suitable_parts, "no memory in requested size range" + part_size, part_pn, part_datasheet, part_lcsc, part_lcsc_basic = suitable_parts[0] - self.assign(self.actual_size, part_size) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', - { - '1': self.spi.miso, # DO - '2': self.gnd, - '3': self.spi.mosi, # DI - '4': self.spi.sck, - '5': self.cs, - '6': self.vcc, - }, - mfr='Microchip Technology', part=part_pn, - datasheet=part_datasheet - ) - self.assign(self.lcsc_part, part_lcsc) - self.assign(self.actual_basic_part, part_lcsc_basic) + self.assign(self.actual_size, part_size) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23-6", + { + "1": self.spi.miso, # DO + "2": self.gnd, + "3": self.spi.mosi, # DI + "4": self.spi.sck, + "5": self.cs, + "6": self.vcc, + }, + mfr="Microchip Technology", + part=part_pn, + datasheet=part_datasheet, + ) + self.assign(self.lcsc_part, part_lcsc) + self.assign(self.actual_basic_part, part_lcsc_basic) class E93Lc_B(SpiMemory): - """93LCxxB series of SPI EEPROMs. The E prefix is because Python identifiers can't start with numbers - Note, A variant is 8-bit word, B variant is 16-bit word - """ - @override - def contents(self) -> None: - super().contents() + """93LCxxB series of SPI EEPROMs. The E prefix is because Python identifiers can't start with numbers + Note, A variant is 8-bit word, B variant is 16-bit word + """ - self.ic = self.Block(E93Lc_B_Device(self.size)) - self.assign(self.actual_size, self.ic.actual_size) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.gnd) - self.connect(self.spi, self.ic.spi) - self.connect(self.cs, self.ic.cs) + @override + def contents(self) -> None: + super().contents() - self.vcc_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2) - )).connected(self.gnd, self.pwr) + self.ic = self.Block(E93Lc_B_Device(self.size)) + self.assign(self.actual_size, self.ic.actual_size) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.gnd) + self.connect(self.spi, self.ic.spi) + self.connect(self.cs, self.ic.cs) + + self.vcc_cap = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) diff --git a/edg/parts/SpiMemory_W25q.py b/edg/parts/SpiMemory_W25q.py index 41e5fd6e6..4f58e9256 100644 --- a/edg/parts/SpiMemory_W25q.py +++ b/edg/parts/SpiMemory_W25q.py @@ -7,99 +7,120 @@ class W25q_Device(InternalSubcircuit, GeneratorBlock, JlcPart, FootprintBlock): - PARTS = [ - # prefer the basic part version - (128*1024*1024, 'W25Q128JVSIQ', 'https://www.winbond.com/resource-files/W25Q128JV%20RevI%2008232021%20Plus.pdf', - 'C97521', True), - # note, 8Mib version considered obsolete at DigiKey - (16*1024*1024, 'W25Q16JVSSIQ', 'https://www.winbond.com/resource-files/w25q16jv%20spi%20revg%2003222018%20plus.pdf', - 'C82317', False), - (32*1024*1024, 'W25Q32JVSSIQ', 'https://www.winbond.com/resource-files/w25q32jv%20revg%2003272018%20plus.pdf', - 'C82344', False), - (64*1024*1024, 'W25Q64JVSSIQ', 'https://www.winbond.com/resource-files/w25q64jv%20revj%2003272018%20plus.pdf', - 'C179171', False), - # higher capacity variants available but not in SOIC-8 - ] - - def __init__(self, size: RangeLike): - super().__init__() - self.vcc = self.Port(VoltageSink( - voltage_limits=(2.7, 3.6)*Volt, # relaxed range that goes up to 104MHz - current_draw=(0.001, 25)*mAmp # typ. power down to max write / erase - ), [Power]) - self.gnd = self.Port(Ground(), [Common]) - - dio_model = DigitalBidir.from_supply( - self.gnd, self.vcc, - voltage_limit_tolerance=(-0.5, 0.4), - input_threshold_factor=(0.3, 0.7) - ) - self.spi = self.Port(SpiPeripheral(dio_model, (0, 104) * MHertz)) - self.cs = self.Port(dio_model) - self.wp = self.Port(dio_model) - self.hold = self.Port(dio_model) - - self.actual_size = self.Parameter(IntExpr()) - - self.size = self.ArgParameter(size) - self.generator_param(self.size) - - @override - def generate(self) -> None: - super().generate() - suitable_parts = [part for part in self.PARTS if part[0] in self.get(self.size)] - assert suitable_parts, "no memory in requested size range" - part_size, part_pn, part_datasheet, part_lcsc, part_lcsc_basic = suitable_parts[0] - - self.assign(self.actual_size, part_size) - self.footprint( - 'U', 'Package_SO:SOIC-8_5.23x5.23mm_P1.27mm', - { - '1': self.cs, - '2': self.spi.miso, - '3': self.wp, - '4': self.gnd, - '5': self.spi.mosi, - '6': self.spi.sck, - '7': self.hold, - '8': self.vcc, - }, - mfr='Winbond Electronics', part=part_pn, - datasheet=part_datasheet - ) - self.assign(self.lcsc_part, part_lcsc) - self.assign(self.actual_basic_part, part_lcsc_basic) + PARTS = [ + # prefer the basic part version + ( + 128 * 1024 * 1024, + "W25Q128JVSIQ", + "https://www.winbond.com/resource-files/W25Q128JV%20RevI%2008232021%20Plus.pdf", + "C97521", + True, + ), + # note, 8Mib version considered obsolete at DigiKey + ( + 16 * 1024 * 1024, + "W25Q16JVSSIQ", + "https://www.winbond.com/resource-files/w25q16jv%20spi%20revg%2003222018%20plus.pdf", + "C82317", + False, + ), + ( + 32 * 1024 * 1024, + "W25Q32JVSSIQ", + "https://www.winbond.com/resource-files/w25q32jv%20revg%2003272018%20plus.pdf", + "C82344", + False, + ), + ( + 64 * 1024 * 1024, + "W25Q64JVSSIQ", + "https://www.winbond.com/resource-files/w25q64jv%20revj%2003272018%20plus.pdf", + "C179171", + False, + ), + # higher capacity variants available but not in SOIC-8 + ] + + def __init__(self, size: RangeLike): + super().__init__() + self.vcc = self.Port( + VoltageSink( + voltage_limits=(2.7, 3.6) * Volt, # relaxed range that goes up to 104MHz + current_draw=(0.001, 25) * mAmp, # typ. power down to max write / erase + ), + [Power], + ) + self.gnd = self.Port(Ground(), [Common]) + + dio_model = DigitalBidir.from_supply( + self.gnd, self.vcc, voltage_limit_tolerance=(-0.5, 0.4), input_threshold_factor=(0.3, 0.7) + ) + self.spi = self.Port(SpiPeripheral(dio_model, (0, 104) * MHertz)) + self.cs = self.Port(dio_model) + self.wp = self.Port(dio_model) + self.hold = self.Port(dio_model) + + self.actual_size = self.Parameter(IntExpr()) + + self.size = self.ArgParameter(size) + self.generator_param(self.size) + + @override + def generate(self) -> None: + super().generate() + suitable_parts = [part for part in self.PARTS if part[0] in self.get(self.size)] + assert suitable_parts, "no memory in requested size range" + part_size, part_pn, part_datasheet, part_lcsc, part_lcsc_basic = suitable_parts[0] + + self.assign(self.actual_size, part_size) + self.footprint( + "U", + "Package_SO:SOIC-8_5.23x5.23mm_P1.27mm", + { + "1": self.cs, + "2": self.spi.miso, + "3": self.wp, + "4": self.gnd, + "5": self.spi.mosi, + "6": self.spi.sck, + "7": self.hold, + "8": self.vcc, + }, + mfr="Winbond Electronics", + part=part_pn, + datasheet=part_datasheet, + ) + self.assign(self.lcsc_part, part_lcsc) + self.assign(self.actual_basic_part, part_lcsc_basic) class W25q(SpiMemory, SpiMemoryQspi, GeneratorBlock): - """Winbond W25Q series of SPI memory devices - """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.generator_param(self.io2.is_connected(), self.io3.is_connected()) - - @override - def contents(self) -> None: - super().contents() - - self.ic = self.Block(W25q_Device(self.size)) - self.assign(self.actual_size, self.ic.actual_size) - self.connect(self.pwr, self.ic.vcc) - self.connect(self.gnd, self.ic.gnd) - self.connect(self.spi, self.ic.spi) - self.connect(self.cs, self.ic.cs) - - self.vcc_cap = self.Block(DecouplingCapacitor( - capacitance=0.1*uFarad(tol=0.2) - )).connected(self.gnd, self.pwr) - - @override - def generate(self) -> None: - super().generate() - - self.require(self.io2.is_connected() == self.io3.is_connected()) - if self.get(self.io2.is_connected()): # connect QSPI lines if used - self.connect(self.io2, self.ic.wp) - self.connect(self.io3, self.ic.hold) - else: # otherwise tie high by default - self.connect(self.pwr.as_digital_source(), self.ic.wp, self.ic.hold) + """Winbond W25Q series of SPI memory devices""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generator_param(self.io2.is_connected(), self.io3.is_connected()) + + @override + def contents(self) -> None: + super().contents() + + self.ic = self.Block(W25q_Device(self.size)) + self.assign(self.actual_size, self.ic.actual_size) + self.connect(self.pwr, self.ic.vcc) + self.connect(self.gnd, self.ic.gnd) + self.connect(self.spi, self.ic.spi) + self.connect(self.cs, self.ic.cs) + + self.vcc_cap = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))).connected(self.gnd, self.pwr) + + @override + def generate(self) -> None: + super().generate() + + self.require(self.io2.is_connected() == self.io3.is_connected()) + if self.get(self.io2.is_connected()): # connect QSPI lines if used + self.connect(self.io2, self.ic.wp) + self.connect(self.io3, self.ic.hold) + else: # otherwise tie high by default + self.connect(self.pwr.as_digital_source(), self.ic.wp, self.ic.hold) diff --git a/edg/parts/StepperDriver_A4988.py b/edg/parts/StepperDriver_A4988.py index f6648390f..8ae98df88 100644 --- a/edg/parts/StepperDriver_A4988.py +++ b/edg/parts/StepperDriver_A4988.py @@ -5,304 +5,316 @@ class A4988_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - - self.gnd = self.Port(Ground()) - vbb_model = VoltageSink( - voltage_limits=(8, 35)*Volt, # in operation; down to 0 during sleep - current_draw=RangeExpr() - ) - kVbbDraw = (0.01, 4)*mAmp - self.vbb1 = self.Port(vbb_model) - self.vbb2 = self.Port(vbb_model) - self.vdd = self.Port(VoltageSink( - voltage_limits=(3, 5.5)*Volt, - current_draw=(0.010, 8)*mAmp - )) - self.vreg = self.Port(VoltageSource( - voltage_out=(7, 7)*Volt, # "nominal output voltage" - current_limits=0*Amp(tol=0) # regulator decoupling terminal only - )) - self.vcp = self.Port(Passive()) - self.cp1 = self.Port(Passive()) - self.cp2 = self.Port(Passive()) - - self.rosc = self.Port(Passive()) - self.ref = self.Port(AnalogSink( - voltage_limits=(0, 5.5)*Volt, - signal_limits=(0, 4)*Volt, - impedance=1.3*MOhm(tol=0) # assumed, from input current @ max voltage - )) - self.sense1 = self.Port(Passive()) - self.sense2 = self.Port(Passive()) - - din_model = DigitalSink.from_supply( - self.gnd, self.vdd, - voltage_limit_abs=(-0.3, 5.5)*Volt, - input_threshold_factor=(0.3, 0.7) - ) - self.ms1 = self.Port(din_model) - self.ms2 = self.Port(din_model) - self.ms3 = self.Port(din_model) - - self.reset = self.Port(din_model) - self.sleep = self.Port(din_model) - self.step = self.Port(din_model) - self.dir = self.Port(din_model) - self.enable = self.Port(din_model) - - dout1_model = DigitalSource.from_supply( - self.gnd, self.vbb1, - current_limits=(-2, 2)*Amp - ) - self.out1a = self.Port(dout1_model) - self.out1b = self.Port(dout1_model) - self.assign(self.vbb1.current_draw, - self.out1a.link().current_drawn.hull(self.out1b.link().current_drawn).abs().hull((0, 0)) + kVbbDraw) - dout2_model = DigitalSource.from_supply( - self.gnd, self.vbb2, - current_limits=(-2, 2)*Amp - ) - self.out2a = self.Port(dout2_model) - self.out2b = self.Port(dout2_model) - self.assign(self.vbb2.current_draw, - self.out2a.link().current_drawn.hull(self.out2b.link().current_drawn).abs().hull((0, 0)) + kVbbDraw) - - @override - def contents(self) -> None: - self.footprint( - 'U', 'Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm_ThermalVias', - { - '4': self.cp1, - '5': self.cp2, - '6': self.vcp, - # pin 7 NC - '8': self.vreg, - '9': self.ms1, - '10': self.ms2, - '11': self.ms3, - '12': self.reset, - '13': self.rosc, - '14': self.sleep, - '15': self.vdd, - '16': self.step, - '17': self.ref, - '3': self.gnd, - '18': self.gnd, - '19': self.dir, - # pin 20 NC - '21': self.out1b, - '22': self.vbb1, - '23': self.sense1, - '24': self.out1a, - # pin 25 NC - '26': self.out2a, - '27': self.sense2, - '28': self.vbb2, - '1': self.out2b, - '2': self.enable, - '29': self.gnd, # GNDs must be tied together externally by connecting to PAD GND - }, - mfr='Allegro MicroSystems', part='A4988SETTR-R', - datasheet='https://www.allegromicro.com/-/media/files/datasheets/a4988-datasheet.pdf' - ) - self.assign(self.lcsc_part, 'C38437') + def __init__(self) -> None: + super().__init__() + + self.gnd = self.Port(Ground()) + vbb_model = VoltageSink( + voltage_limits=(8, 35) * Volt, current_draw=RangeExpr() # in operation; down to 0 during sleep + ) + kVbbDraw = (0.01, 4) * mAmp + self.vbb1 = self.Port(vbb_model) + self.vbb2 = self.Port(vbb_model) + self.vdd = self.Port(VoltageSink(voltage_limits=(3, 5.5) * Volt, current_draw=(0.010, 8) * mAmp)) + self.vreg = self.Port( + VoltageSource( + voltage_out=(7, 7) * Volt, # "nominal output voltage" + current_limits=0 * Amp(tol=0), # regulator decoupling terminal only + ) + ) + self.vcp = self.Port(Passive()) + self.cp1 = self.Port(Passive()) + self.cp2 = self.Port(Passive()) + + self.rosc = self.Port(Passive()) + self.ref = self.Port( + AnalogSink( + voltage_limits=(0, 5.5) * Volt, + signal_limits=(0, 4) * Volt, + impedance=1.3 * MOhm(tol=0), # assumed, from input current @ max voltage + ) + ) + self.sense1 = self.Port(Passive()) + self.sense2 = self.Port(Passive()) + + din_model = DigitalSink.from_supply( + self.gnd, self.vdd, voltage_limit_abs=(-0.3, 5.5) * Volt, input_threshold_factor=(0.3, 0.7) + ) + self.ms1 = self.Port(din_model) + self.ms2 = self.Port(din_model) + self.ms3 = self.Port(din_model) + + self.reset = self.Port(din_model) + self.sleep = self.Port(din_model) + self.step = self.Port(din_model) + self.dir = self.Port(din_model) + self.enable = self.Port(din_model) + + dout1_model = DigitalSource.from_supply(self.gnd, self.vbb1, current_limits=(-2, 2) * Amp) + self.out1a = self.Port(dout1_model) + self.out1b = self.Port(dout1_model) + self.assign( + self.vbb1.current_draw, + self.out1a.link().current_drawn.hull(self.out1b.link().current_drawn).abs().hull((0, 0)) + kVbbDraw, + ) + dout2_model = DigitalSource.from_supply(self.gnd, self.vbb2, current_limits=(-2, 2) * Amp) + self.out2a = self.Port(dout2_model) + self.out2b = self.Port(dout2_model) + self.assign( + self.vbb2.current_draw, + self.out2a.link().current_drawn.hull(self.out2b.link().current_drawn).abs().hull((0, 0)) + kVbbDraw, + ) + + @override + def contents(self) -> None: + self.footprint( + "U", + "Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm_ThermalVias", + { + "4": self.cp1, + "5": self.cp2, + "6": self.vcp, + # pin 7 NC + "8": self.vreg, + "9": self.ms1, + "10": self.ms2, + "11": self.ms3, + "12": self.reset, + "13": self.rosc, + "14": self.sleep, + "15": self.vdd, + "16": self.step, + "17": self.ref, + "3": self.gnd, + "18": self.gnd, + "19": self.dir, + # pin 20 NC + "21": self.out1b, + "22": self.vbb1, + "23": self.sense1, + "24": self.out1a, + # pin 25 NC + "26": self.out2a, + "27": self.sense2, + "28": self.vbb2, + "1": self.out2b, + "2": self.enable, + "29": self.gnd, # GNDs must be tied together externally by connecting to PAD GND + }, + mfr="Allegro MicroSystems", + part="A4988SETTR-R", + datasheet="https://www.allegromicro.com/-/media/files/datasheets/a4988-datasheet.pdf", + ) + self.assign(self.lcsc_part, "C38437") class A4988(GeneratorBlock): - """Bipolar stepper motor driver with microstepping (1:2/4/8/16) and current limiting. 8-35V input, up to 2A.""" - def __init__(self, step_resolution: IntLike = 16, - itrip: RangeLike = 1*Amp(tol=0.15), - itrip_vref: RangeLike = 0.25*Volt(tol=0.08)) -> None: - super().__init__() - self.step_resolution = self.ArgParameter(step_resolution, doc="microstepping resolution (1, 2, 4, 8, or 16)") - self.itrip = self.ArgParameter(itrip, doc="maximum (trip) current across motor windings") - self.itrip_vref = self.ArgParameter(itrip_vref, doc="voltage reference for Isense trip, not counting the 8x on Vref") - self.generator_param(self.step_resolution) - - self.ic = self.Block(A4988_Device()) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.pwr = self.Export(self.ic.vbb1) - self.pwr_logic = self.Export(self.ic.vdd) - - self.step = self.Export(self.ic.step) - self.dir = self.Export(self.ic.dir) - - self.enable = self.Port(DigitalSink.empty(), optional=True, doc="disables FET outputs when high") - self.reset = self.Port(DigitalSink.empty(), optional=True, doc="forces translator to Home state when low") - self.sleep = self.Port(DigitalSink.empty(), optional=True, doc="disables device (to reduce current draw) when low") - self.generator_param(self.enable.is_connected(), self.reset.is_connected(), self.sleep.is_connected()) - - self.out1a = self.Export(self.ic.out1a) - self.out1b = self.Export(self.ic.out1b) - self.out2a = self.Export(self.ic.out2a) - self.out2b = self.Export(self.ic.out2b) - - @override - def contents(self) -> None: - super().contents() - - # the upper tolerable range of these caps is extended to allow search flexibility when voltage derating - self.vreg_cap = self.Block(DecouplingCapacitor((0.22*0.8, 1)*uFarad)).connected(self.gnd, self.ic.vreg) - self.vdd_cap = self.Block(DecouplingCapacitor((0.22*0.8, 1)*uFarad)).connected(self.gnd, self.pwr_logic) - - self.vcpi_cap = self.Block(Capacitor(0.1*uFarad(tol=0.2), (0, 16)*Volt)) # voltage a wild guess, no specs given - self.connect(self.vcpi_cap.pos, self.ic.cp1) - self.connect(self.vcpi_cap.neg, self.ic.cp2) - self.vcp_cap = self.Block(Capacitor(0.1*uFarad(tol=0.2), (0, 16)*Volt)) - self.connect(self.vcp_cap.pos, self.ic.vcp) - self.connect(self.vcp_cap.neg.adapt_to(VoltageSink()), self.ic.vbb1, self.ic.vbb2) - - self.rosc = self.Block(Resistor(10*kOhm(tol=0.05))) # arbitrary, from Pololu breakout board - self.connect(self.rosc.a.adapt_to(Ground()), self.gnd) - self.connect(self.rosc.b, self.ic.rosc) - - self.ref_div = self.Block(VoltageDivider(output_voltage=self.itrip_vref * 8, impedance=(1, 10)*kOhm)) - self.connect(self.ref_div.gnd, self.gnd) - self.connect(self.ref_div.input, self.pwr_logic) - self.connect(self.ref_div.output, self.ic.ref) - - self.isense = ElementDict[Resistor]() - for i, sensen in [('1', self.ic.sense1), ('2', self.ic.sense2)]: - isense = self.isense[i] = self.Block(Resistor( - self.itrip_vref / self.itrip, # TODO shrink tolerances - power=self.itrip_vref * self.itrip - )) - self.connect(isense.a, sensen) - self.connect(isense.b.adapt_to(Ground()), self.gnd) - - @override - def generate(self) -> None: - super().generate() - - step_resolution = self.get(self.step_resolution) - if step_resolution == 1: # full step - self.connect(self.gnd.as_digital_source(), self.ic.ms1, self.ic.ms2, self.ic.ms3) - elif step_resolution == 2: # half step - self.connect(self.gnd.as_digital_source(), self.ic.ms2, self.ic.ms3) - self.connect(self.pwr_logic.as_digital_source(), self.ic.ms1) - elif step_resolution == 4: # quarter step - self.connect(self.gnd.as_digital_source(), self.ic.ms1, self.ic.ms3) - self.connect(self.pwr_logic.as_digital_source(), self.ic.ms2) - elif step_resolution == 8: # eighth step - self.connect(self.gnd.as_digital_source(), self.ic.ms3) - self.connect(self.pwr_logic.as_digital_source(), self.ic.ms1, self.ic.ms2) - elif step_resolution == 16: # sixteenth step - self.connect(self.pwr_logic.as_digital_source(), self.ic.ms1, self.ic.ms2, self.ic.ms3) - else: - raise ValueError(f"unknown step_resolution {step_resolution}") - - if self.get(self.enable.is_connected()): - self.connect(self.enable, self.ic.enable) - else: - self.connect(self.gnd.as_digital_source(), self.ic.enable) - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.ic.reset) - else: - self.connect(self.pwr_logic.as_digital_source(), self.ic.reset) - - if self.get(self.sleep.is_connected()): - self.connect(self.sleep, self.ic.sleep) - else: - self.connect(self.pwr_logic.as_digital_source(), self.ic.sleep) + """Bipolar stepper motor driver with microstepping (1:2/4/8/16) and current limiting. 8-35V input, up to 2A.""" + + def __init__( + self, + step_resolution: IntLike = 16, + itrip: RangeLike = 1 * Amp(tol=0.15), + itrip_vref: RangeLike = 0.25 * Volt(tol=0.08), + ) -> None: + super().__init__() + self.step_resolution = self.ArgParameter(step_resolution, doc="microstepping resolution (1, 2, 4, 8, or 16)") + self.itrip = self.ArgParameter(itrip, doc="maximum (trip) current across motor windings") + self.itrip_vref = self.ArgParameter( + itrip_vref, doc="voltage reference for Isense trip, not counting the 8x on Vref" + ) + self.generator_param(self.step_resolution) + + self.ic = self.Block(A4988_Device()) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.pwr = self.Export(self.ic.vbb1) + self.pwr_logic = self.Export(self.ic.vdd) + + self.step = self.Export(self.ic.step) + self.dir = self.Export(self.ic.dir) + + self.enable = self.Port(DigitalSink.empty(), optional=True, doc="disables FET outputs when high") + self.reset = self.Port(DigitalSink.empty(), optional=True, doc="forces translator to Home state when low") + self.sleep = self.Port( + DigitalSink.empty(), optional=True, doc="disables device (to reduce current draw) when low" + ) + self.generator_param(self.enable.is_connected(), self.reset.is_connected(), self.sleep.is_connected()) + + self.out1a = self.Export(self.ic.out1a) + self.out1b = self.Export(self.ic.out1b) + self.out2a = self.Export(self.ic.out2a) + self.out2b = self.Export(self.ic.out2b) + + @override + def contents(self) -> None: + super().contents() + + # the upper tolerable range of these caps is extended to allow search flexibility when voltage derating + self.vreg_cap = self.Block(DecouplingCapacitor((0.22 * 0.8, 1) * uFarad)).connected(self.gnd, self.ic.vreg) + self.vdd_cap = self.Block(DecouplingCapacitor((0.22 * 0.8, 1) * uFarad)).connected(self.gnd, self.pwr_logic) + + self.vcpi_cap = self.Block( + Capacitor(0.1 * uFarad(tol=0.2), (0, 16) * Volt) + ) # voltage a wild guess, no specs given + self.connect(self.vcpi_cap.pos, self.ic.cp1) + self.connect(self.vcpi_cap.neg, self.ic.cp2) + self.vcp_cap = self.Block(Capacitor(0.1 * uFarad(tol=0.2), (0, 16) * Volt)) + self.connect(self.vcp_cap.pos, self.ic.vcp) + self.connect(self.vcp_cap.neg.adapt_to(VoltageSink()), self.ic.vbb1, self.ic.vbb2) + + self.rosc = self.Block(Resistor(10 * kOhm(tol=0.05))) # arbitrary, from Pololu breakout board + self.connect(self.rosc.a.adapt_to(Ground()), self.gnd) + self.connect(self.rosc.b, self.ic.rosc) + + self.ref_div = self.Block(VoltageDivider(output_voltage=self.itrip_vref * 8, impedance=(1, 10) * kOhm)) + self.connect(self.ref_div.gnd, self.gnd) + self.connect(self.ref_div.input, self.pwr_logic) + self.connect(self.ref_div.output, self.ic.ref) + + self.isense = ElementDict[Resistor]() + for i, sensen in [("1", self.ic.sense1), ("2", self.ic.sense2)]: + isense = self.isense[i] = self.Block( + Resistor(self.itrip_vref / self.itrip, power=self.itrip_vref * self.itrip) # TODO shrink tolerances + ) + self.connect(isense.a, sensen) + self.connect(isense.b.adapt_to(Ground()), self.gnd) + + @override + def generate(self) -> None: + super().generate() + + step_resolution = self.get(self.step_resolution) + if step_resolution == 1: # full step + self.connect(self.gnd.as_digital_source(), self.ic.ms1, self.ic.ms2, self.ic.ms3) + elif step_resolution == 2: # half step + self.connect(self.gnd.as_digital_source(), self.ic.ms2, self.ic.ms3) + self.connect(self.pwr_logic.as_digital_source(), self.ic.ms1) + elif step_resolution == 4: # quarter step + self.connect(self.gnd.as_digital_source(), self.ic.ms1, self.ic.ms3) + self.connect(self.pwr_logic.as_digital_source(), self.ic.ms2) + elif step_resolution == 8: # eighth step + self.connect(self.gnd.as_digital_source(), self.ic.ms3) + self.connect(self.pwr_logic.as_digital_source(), self.ic.ms1, self.ic.ms2) + elif step_resolution == 16: # sixteenth step + self.connect(self.pwr_logic.as_digital_source(), self.ic.ms1, self.ic.ms2, self.ic.ms3) + else: + raise ValueError(f"unknown step_resolution {step_resolution}") + + if self.get(self.enable.is_connected()): + self.connect(self.enable, self.ic.enable) + else: + self.connect(self.gnd.as_digital_source(), self.ic.enable) + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.ic.reset) + else: + self.connect(self.pwr_logic.as_digital_source(), self.ic.reset) + + if self.get(self.sleep.is_connected()): + self.connect(self.sleep, self.ic.sleep) + else: + self.connect(self.pwr_logic.as_digital_source(), self.ic.sleep) class PololuA4988(BrushedMotorDriver, WrapperFootprintBlock, GeneratorBlock): - """Pololu breakout board for the A4988 stepper driver. Adjustable current limit with onboard trimpot.""" - def __init__(self, step_resolution: IntLike = 16): - super().__init__() - self.step_resolution = self.ArgParameter(step_resolution, doc="microstepping resolution (1, 2, 4, 8, or 16)") - self.generator_param(self.step_resolution) - - self.model = self.Block(A4988_Device()) - self.gnd = self.Export(self.model.gnd, [Common]) - self.pwr = self.Export(self.model.vbb1) - self.pwr_logic = self.Export(self.model.vdd) - - self.step = self.Export(self.model.step) - self.dir = self.Export(self.model.dir) - - self.enable = self.Port(DigitalSink.empty(), optional=True, doc="disables FET outputs when high") - self.reset = self.Port(DigitalSink.empty(), optional=True, doc="forces translator to Home state when low") - self.sleep = self.Port(DigitalSink.empty(), optional=True, doc="disables device (to reduce current draw) when low") - self.generator_param(self.enable.is_connected(), self.reset.is_connected(), self.sleep.is_connected()) - - self.out1a = self.Export(self.model.out1a) - self.out1b = self.Export(self.model.out1b) - self.out2a = self.Export(self.model.out2a) - self.out2b = self.Export(self.model.out2b) - - @override - def generate(self) -> None: - super().generate() - - self.connect(self.pwr, self.model.vbb2) - - # TODO: deduplicate w/ A4988 application circuit - step_resolution = self.get(self.step_resolution) - if step_resolution == 1: # full step - self.connect(self.gnd.as_digital_source(), self.model.ms1, self.model.ms2, self.model.ms3) - elif step_resolution == 2: # half step - self.connect(self.gnd.as_digital_source(), self.model.ms2, self.model.ms3) - self.connect(self.pwr_logic.as_digital_source(), self.model.ms1) - elif step_resolution == 4: # quarter step - self.connect(self.gnd.as_digital_source(), self.model.ms1, self.model.ms3) - self.connect(self.pwr_logic.as_digital_source(), self.model.ms2) - elif step_resolution == 8: # eighth step - self.connect(self.gnd.as_digital_source(), self.model.ms3) - self.connect(self.pwr_logic.as_digital_source(), self.model.ms1, self.model.ms2) - elif step_resolution == 16: # sixteenth step - self.connect(self.pwr_logic.as_digital_source(), self.model.ms1, self.model.ms2, self.model.ms3) - else: - raise ValueError(f"unknown step_resolution {step_resolution}") - - if self.get(self.enable.is_connected()): - self.connect(self.enable, self.model.enable) - else: - self.connect(self.gnd.as_digital_source(), self.model.enable) - - if self.get(self.reset.is_connected()): - self.connect(self.reset, self.model.reset) - else: - self.connect(self.pwr_logic.as_digital_source(), self.model.reset) - - if self.get(self.sleep.is_connected()): - self.connect(self.sleep, self.model.sleep) - else: - self.connect(self.pwr_logic.as_digital_source(), self.model.sleep) - - # these are implemented internal to the breakout board - (self.dummy_vreg, ), _ = self.chain(self.Block(DummyVoltageSink()), self.model.vreg) - (self.dummy_vcp, ), _ = self.chain(self.Block(DummyPassive()), self.model.vcp) - (self.dummy_cp1, ), _ = self.chain(self.Block(DummyPassive()), self.model.cp1) - (self.dummy_cp2, ), _ = self.chain(self.Block(DummyPassive()), self.model.cp2) - (self.dummy_rosc, ), _ = self.chain(self.Block(DummyPassive()), self.model.rosc) - (self.dummy_ref, ), _ = self.chain(self.Block(DummyAnalogSource()), self.model.ref) - (self.dummy_sense1, ), _ = self.chain(self.Block(DummyPassive()), self.model.sense1) - (self.dummy_sense2, ), _ = self.chain(self.Block(DummyPassive()), self.model.sense2) - - self.footprint( - 'U', 'edg:DIP-16_W12.70mm', - { - '1': self.pwr, - '2': self.gnd, - '3': self.out2b, - '4': self.out2a, - '5': self.out1a, - '6': self.out1b, - '7': self.pwr_logic, - '8': self.gnd, - '9': self.dir, - '10': self.step, - '11': self.model.sleep, - '12': self.model.reset, - '13': self.model.ms3, - '14': self.model.ms2, - '15': self.model.ms1, - '16': self.model.enable, - }, - mfr='Pololu', part='1182', - datasheet='https://www.pololu.com/product/1182' - ) + """Pololu breakout board for the A4988 stepper driver. Adjustable current limit with onboard trimpot.""" + + def __init__(self, step_resolution: IntLike = 16): + super().__init__() + self.step_resolution = self.ArgParameter(step_resolution, doc="microstepping resolution (1, 2, 4, 8, or 16)") + self.generator_param(self.step_resolution) + + self.model = self.Block(A4988_Device()) + self.gnd = self.Export(self.model.gnd, [Common]) + self.pwr = self.Export(self.model.vbb1) + self.pwr_logic = self.Export(self.model.vdd) + + self.step = self.Export(self.model.step) + self.dir = self.Export(self.model.dir) + + self.enable = self.Port(DigitalSink.empty(), optional=True, doc="disables FET outputs when high") + self.reset = self.Port(DigitalSink.empty(), optional=True, doc="forces translator to Home state when low") + self.sleep = self.Port( + DigitalSink.empty(), optional=True, doc="disables device (to reduce current draw) when low" + ) + self.generator_param(self.enable.is_connected(), self.reset.is_connected(), self.sleep.is_connected()) + + self.out1a = self.Export(self.model.out1a) + self.out1b = self.Export(self.model.out1b) + self.out2a = self.Export(self.model.out2a) + self.out2b = self.Export(self.model.out2b) + + @override + def generate(self) -> None: + super().generate() + + self.connect(self.pwr, self.model.vbb2) + + # TODO: deduplicate w/ A4988 application circuit + step_resolution = self.get(self.step_resolution) + if step_resolution == 1: # full step + self.connect(self.gnd.as_digital_source(), self.model.ms1, self.model.ms2, self.model.ms3) + elif step_resolution == 2: # half step + self.connect(self.gnd.as_digital_source(), self.model.ms2, self.model.ms3) + self.connect(self.pwr_logic.as_digital_source(), self.model.ms1) + elif step_resolution == 4: # quarter step + self.connect(self.gnd.as_digital_source(), self.model.ms1, self.model.ms3) + self.connect(self.pwr_logic.as_digital_source(), self.model.ms2) + elif step_resolution == 8: # eighth step + self.connect(self.gnd.as_digital_source(), self.model.ms3) + self.connect(self.pwr_logic.as_digital_source(), self.model.ms1, self.model.ms2) + elif step_resolution == 16: # sixteenth step + self.connect(self.pwr_logic.as_digital_source(), self.model.ms1, self.model.ms2, self.model.ms3) + else: + raise ValueError(f"unknown step_resolution {step_resolution}") + + if self.get(self.enable.is_connected()): + self.connect(self.enable, self.model.enable) + else: + self.connect(self.gnd.as_digital_source(), self.model.enable) + + if self.get(self.reset.is_connected()): + self.connect(self.reset, self.model.reset) + else: + self.connect(self.pwr_logic.as_digital_source(), self.model.reset) + + if self.get(self.sleep.is_connected()): + self.connect(self.sleep, self.model.sleep) + else: + self.connect(self.pwr_logic.as_digital_source(), self.model.sleep) + + # these are implemented internal to the breakout board + (self.dummy_vreg,), _ = self.chain(self.Block(DummyVoltageSink()), self.model.vreg) + (self.dummy_vcp,), _ = self.chain(self.Block(DummyPassive()), self.model.vcp) + (self.dummy_cp1,), _ = self.chain(self.Block(DummyPassive()), self.model.cp1) + (self.dummy_cp2,), _ = self.chain(self.Block(DummyPassive()), self.model.cp2) + (self.dummy_rosc,), _ = self.chain(self.Block(DummyPassive()), self.model.rosc) + (self.dummy_ref,), _ = self.chain(self.Block(DummyAnalogSource()), self.model.ref) + (self.dummy_sense1,), _ = self.chain(self.Block(DummyPassive()), self.model.sense1) + (self.dummy_sense2,), _ = self.chain(self.Block(DummyPassive()), self.model.sense2) + + self.footprint( + "U", + "edg:DIP-16_W12.70mm", + { + "1": self.pwr, + "2": self.gnd, + "3": self.out2b, + "4": self.out2a, + "5": self.out1a, + "6": self.out1b, + "7": self.pwr_logic, + "8": self.gnd, + "9": self.dir, + "10": self.step, + "11": self.model.sleep, + "12": self.model.reset, + "13": self.model.ms3, + "14": self.model.ms2, + "15": self.model.ms1, + "16": self.model.enable, + }, + mfr="Pololu", + part="1182", + datasheet="https://www.pololu.com/product/1182", + ) diff --git a/edg/parts/SwitchMatrix.py b/edg/parts/SwitchMatrix.py index 46ad77761..c7c82a4e3 100644 --- a/edg/parts/SwitchMatrix.py +++ b/edg/parts/SwitchMatrix.py @@ -6,36 +6,37 @@ class SwitchMatrix(HumanInterface, GeneratorBlock, SvgPcbTemplateBlock): - """A switch matrix, such as for a keyboard, that generates (nrows * ncols) switches while only - using max(nrows, ncols) IOs. - - Internally, the switches are in a matrix, with the driver driving one col low at a time while - reading which rows are low (with the other cols weakly pulled high). - This uses the Switch abstract class, which can be refined into e.g. a tactile switch or mechanical keyswitch. - - This generates per-switch diodes which allows multiple keys to be pressed simultaneously. - Diode anodes are attached to the rows, while cathodes go through each switch to the cols. - """ - @override - def _svgpcb_fn_name_adds(self) -> Optional[str]: - return f"{self._svgpcb_get(self.ncols)}_{self._svgpcb_get(self.nrows)}" - - @override - def _svgpcb_template(self) -> str: - switch_block = self._svgpcb_footprint_block_path_of(['sw[0,0]']) - diode_block = self._svgpcb_footprint_block_path_of(['d[0,0]']) - switch_reftype, switch_refnum = self._svgpcb_refdes_of(['sw[0,0]']) - diode_reftype, diode_refnum = self._svgpcb_refdes_of(['d[0,0]']) - assert switch_block is not None and diode_block is not None - switch_footprint = self._svgpcb_footprint_of(switch_block) - switch_sw_pin = self._svgpcb_pin_of(['sw[0,0]'], ['sw']) - switch_com_pin = self._svgpcb_pin_of(['sw[0,0]'], ['com']) - diode_footprint = self._svgpcb_footprint_of(diode_block) - diode_a_pin = self._svgpcb_pin_of(['d[0,0]'], ['anode']) - diode_k_pin = self._svgpcb_pin_of(['d[0,0]'], ['cathode']) - assert all([pin is not None for pin in [switch_sw_pin, switch_com_pin, diode_a_pin, diode_k_pin]]) - - return f"""\ + """A switch matrix, such as for a keyboard, that generates (nrows * ncols) switches while only + using max(nrows, ncols) IOs. + + Internally, the switches are in a matrix, with the driver driving one col low at a time while + reading which rows are low (with the other cols weakly pulled high). + This uses the Switch abstract class, which can be refined into e.g. a tactile switch or mechanical keyswitch. + + This generates per-switch diodes which allows multiple keys to be pressed simultaneously. + Diode anodes are attached to the rows, while cathodes go through each switch to the cols. + """ + + @override + def _svgpcb_fn_name_adds(self) -> Optional[str]: + return f"{self._svgpcb_get(self.ncols)}_{self._svgpcb_get(self.nrows)}" + + @override + def _svgpcb_template(self) -> str: + switch_block = self._svgpcb_footprint_block_path_of(["sw[0,0]"]) + diode_block = self._svgpcb_footprint_block_path_of(["d[0,0]"]) + switch_reftype, switch_refnum = self._svgpcb_refdes_of(["sw[0,0]"]) + diode_reftype, diode_refnum = self._svgpcb_refdes_of(["d[0,0]"]) + assert switch_block is not None and diode_block is not None + switch_footprint = self._svgpcb_footprint_of(switch_block) + switch_sw_pin = self._svgpcb_pin_of(["sw[0,0]"], ["sw"]) + switch_com_pin = self._svgpcb_pin_of(["sw[0,0]"], ["com"]) + diode_footprint = self._svgpcb_footprint_of(diode_block) + diode_a_pin = self._svgpcb_pin_of(["d[0,0]"], ["anode"]) + diode_k_pin = self._svgpcb_pin_of(["d[0,0]"], ["cathode"]) + assert all([pin is not None for pin in [switch_sw_pin, switch_com_pin, diode_a_pin, diode_k_pin]]) + + return f"""\ function {self._svgpcb_fn_name()}(xy, colSpacing=0.5, rowSpacing=0.5, diodeOffset=[0.25, 0]) {{ // Circuit generator params const ncols = {self._svgpcb_get(self.ncols)} @@ -107,54 +108,67 @@ def _svgpcb_template(self) -> str: }} """ - @override - def _svgpcb_bbox(self) -> Tuple[float, float, float, float]: - return (-1.0, -1.0, - self._svgpcb_get(self.ncols) * 0.5 * 25.4 + 1.0, (self._svgpcb_get(self.nrows) + 1) * .5 * 25.4 + 1.0) - - def __init__(self, nrows: IntLike, ncols: IntLike, voltage_drop: RangeLike = (0, 0.7)*Volt): - super().__init__() - - self.rows = self.Port(Vector(DigitalSource.empty())) - self.cols = self.Port(Vector(DigitalSink.empty())) - self.voltage_drop = self.ArgParameter(voltage_drop) - - self.nrows = self.ArgParameter(nrows) - self.ncols = self.ArgParameter(ncols) - self.generator_param(self.nrows, self.ncols) - - @override - def generate(self) -> None: - super().generate() - row_ports = {} - for row in range(self.get(self.nrows)): - row_ports[row] = self.rows.append_elt(DigitalSource.empty(), str(row)) - - self.sw = ElementDict[Switch]() - self.d = ElementDict[Diode]() - for col in range(self.get(self.ncols)): - col_port = self.cols.append_elt(DigitalSink.empty(), str(col)) - col_port_model = DigitalSink() # ideal, negligible current draw (assumed) and thresholds checked at other side - for (row, row_port) in row_ports.items(): - sw = self.sw[f"{col},{row}"] = self.Block(Switch( - voltage=row_port.link().voltage, - current=row_port.link().current_drawn - )) - d = self.d[f"{col},{row}"] = self.Block(Diode( - current=row_port.link().current_drawn, - # col voltage is used as a proxy, since (properly) using the row voltage causes a circular dependency - reverse_voltage=col_port.link().voltage, - voltage_drop=self.voltage_drop - )) - lowest_output = col_port.link().voltage.lower() + d.actual_voltage_drop.lower() - highest_output = col_port.link().output_thresholds.lower() + d.actual_voltage_drop.upper() - self.connect(d.anode.adapt_to(DigitalSource( - voltage_out=(lowest_output, highest_output), - output_thresholds=(highest_output, float('inf')), - low_driver=True, high_driver=False - )), row_port) - self.connect(d.cathode, sw.sw) - self.connect(sw.com.adapt_to(col_port_model), col_port) - - self.rows.defined() - self.cols.defined() + @override + def _svgpcb_bbox(self) -> Tuple[float, float, float, float]: + return ( + -1.0, + -1.0, + self._svgpcb_get(self.ncols) * 0.5 * 25.4 + 1.0, + (self._svgpcb_get(self.nrows) + 1) * 0.5 * 25.4 + 1.0, + ) + + def __init__(self, nrows: IntLike, ncols: IntLike, voltage_drop: RangeLike = (0, 0.7) * Volt): + super().__init__() + + self.rows = self.Port(Vector(DigitalSource.empty())) + self.cols = self.Port(Vector(DigitalSink.empty())) + self.voltage_drop = self.ArgParameter(voltage_drop) + + self.nrows = self.ArgParameter(nrows) + self.ncols = self.ArgParameter(ncols) + self.generator_param(self.nrows, self.ncols) + + @override + def generate(self) -> None: + super().generate() + row_ports = {} + for row in range(self.get(self.nrows)): + row_ports[row] = self.rows.append_elt(DigitalSource.empty(), str(row)) + + self.sw = ElementDict[Switch]() + self.d = ElementDict[Diode]() + for col in range(self.get(self.ncols)): + col_port = self.cols.append_elt(DigitalSink.empty(), str(col)) + col_port_model = ( + DigitalSink() + ) # ideal, negligible current draw (assumed) and thresholds checked at other side + for row, row_port in row_ports.items(): + sw = self.sw[f"{col},{row}"] = self.Block( + Switch(voltage=row_port.link().voltage, current=row_port.link().current_drawn) + ) + d = self.d[f"{col},{row}"] = self.Block( + Diode( + current=row_port.link().current_drawn, + # col voltage is used as a proxy, since (properly) using the row voltage causes a circular dependency + reverse_voltage=col_port.link().voltage, + voltage_drop=self.voltage_drop, + ) + ) + lowest_output = col_port.link().voltage.lower() + d.actual_voltage_drop.lower() + highest_output = col_port.link().output_thresholds.lower() + d.actual_voltage_drop.upper() + self.connect( + d.anode.adapt_to( + DigitalSource( + voltage_out=(lowest_output, highest_output), + output_thresholds=(highest_output, float("inf")), + low_driver=True, + high_driver=False, + ) + ), + row_port, + ) + self.connect(d.cathode, sw.sw) + self.connect(sw.com.adapt_to(col_port_model), col_port) + + self.rows.defined() + self.cols.defined() diff --git a/edg/parts/SwitchedCap_TexasInstruments.py b/edg/parts/SwitchedCap_TexasInstruments.py index 56790b3fb..2f8e935b7 100644 --- a/edg/parts/SwitchedCap_TexasInstruments.py +++ b/edg/parts/SwitchedCap_TexasInstruments.py @@ -7,21 +7,16 @@ class Lm2664_Device(InternalSubcircuit, JlcPart, FootprintBlock): FREQUENCY = Range(40000, 80000) # Hz SWITCH_RESISTANCE = Range(4, 8) # Ohm + def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground(), [Common]) - self.vp = self.Port(VoltageSink( - voltage_limits=(1.8, 5.5)*Volt, - current_draw=RangeExpr() - ), [Power]) + self.vp = self.Port(VoltageSink(voltage_limits=(1.8, 5.5) * Volt, current_draw=RangeExpr()), [Power]) self.capn = self.Port(Passive()) self.capp = self.Port(Passive()) - self.out = self.Port(VoltageSource( - voltage_out=-self.vp.link().voltage, - current_limits=(0, 40)*mAmp - )) - self.assign(self.vp.current_draw, (1, 500)*uAmp + self.out.link().current_drawn) + self.out = self.Port(VoltageSource(voltage_out=-self.vp.link().voltage, current_limits=(0, 40) * mAmp)) + self.assign(self.vp.current_draw, (1, 500) * uAmp + self.out.link().current_drawn) # self.sd = self.Port(DigitalSink.from_supply( # self.gnd, self.vp, @@ -33,26 +28,28 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23-6', + "U", + "Package_TO_SOT_SMD:SOT-23-6", { - '1': self.gnd, - '2': self.out, - '3': self.capn, - '4': self.vp, # self.sd, - '5': self.vp, - '6': self.capp, + "1": self.gnd, + "2": self.out, + "3": self.capn, + "4": self.vp, # self.sd, + "5": self.vp, + "6": self.capp, }, - mfr='Texas Instruments', part='LM2664', - datasheet='https://www.ti.com/lit/ds/symlink/lm2664.pdf' + mfr="Texas Instruments", + part="LM2664", + datasheet="https://www.ti.com/lit/ds/symlink/lm2664.pdf", ) - self.assign(self.lcsc_part, 'C840095') + self.assign(self.lcsc_part, "C840095") self.assign(self.actual_basic_part, False) class Lm2664(PowerConditioner, Block): """Switched capacitor inverter""" - def __init__(self, output_resistance_limit: FloatLike = 25 * Ohm, - output_ripple_limit: FloatLike = 25 * mVolt): + + def __init__(self, output_resistance_limit: FloatLike = 25 * Ohm, output_ripple_limit: FloatLike = 25 * mVolt): super().__init__() self.ic = self.Block(Lm2664_Device()) self.gnd = self.Export(self.ic.gnd, [Common]) @@ -64,18 +61,27 @@ def __init__(self, output_resistance_limit: FloatLike = 25 * Ohm, @override def contents(self) -> None: super().contents() - self.require(self.output_resistance_limit >= 2 * self.ic.SWITCH_RESISTANCE.upper, - "min output resistance spec below switch resistance") - self.cf = self.Block(Capacitor( - capacitance=(2 / self.ic.FREQUENCY.lower / - (self.output_resistance_limit - 2 * self.ic.SWITCH_RESISTANCE.upper), - float('inf')), - voltage=self.pwr_out.voltage_out - )) + self.require( + self.output_resistance_limit >= 2 * self.ic.SWITCH_RESISTANCE.upper, + "min output resistance spec below switch resistance", + ) + self.cf = self.Block( + Capacitor( + capacitance=( + 2 / self.ic.FREQUENCY.lower / (self.output_resistance_limit - 2 * self.ic.SWITCH_RESISTANCE.upper), + float("inf"), + ), + voltage=self.pwr_out.voltage_out, + ) + ) self.connect(self.cf.neg, self.ic.capn) self.connect(self.cf.pos, self.ic.capp) - self.cout = self.Block(DecouplingCapacitor( - (self.pwr_out.link().current_drawn.upper() / self.ic.FREQUENCY.lower / self.output_ripple_limit, - float('inf')) - )).connected(self.gnd, self.pwr_out) + self.cout = self.Block( + DecouplingCapacitor( + ( + self.pwr_out.link().current_drawn.upper() / self.ic.FREQUENCY.lower / self.output_ripple_limit, + float("inf"), + ) + ) + ).connected(self.gnd, self.pwr_out) diff --git a/edg/parts/Switches.py b/edg/parts/Switches.py index 918ccc844..6acdff942 100644 --- a/edg/parts/Switches.py +++ b/edg/parts/Switches.py @@ -4,56 +4,61 @@ class SmtSwitch(TactileSwitch, FootprintBlock): - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'SW', 'Button_Switch_SMD:SW_Push_SPST_NO_Alps_SKRK', # 3.9mm x 2.9mm - # 'Button_Switch_SMD:SW_SPST_CK_KXT3', # 3.0mm x 2.0mm - { - '1': self.sw, - '2': self.com, - }, - part='3.9x2.9mm Switch' - ) - # the P/N isn't standardized, but these have been used in the past: - # PTS820 J25K SMTR LFS, 2.5mm actuator height (from board) + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "SW", + "Button_Switch_SMD:SW_Push_SPST_NO_Alps_SKRK", # 3.9mm x 2.9mm + # 'Button_Switch_SMD:SW_SPST_CK_KXT3', # 3.0mm x 2.0mm + { + "1": self.sw, + "2": self.com, + }, + part="3.9x2.9mm Switch", + ) + # the P/N isn't standardized, but these have been used in the past: + # PTS820 J25K SMTR LFS, 2.5mm actuator height (from board) class SmtSwitchRa(TactileSwitch, FootprintBlock): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.footprint( - 'SW', 'Button_Switch_SMD:SW_SPST_EVQP7C', # 3.5mm x 2.9/3.55mm w/ boss - { - '1': self.sw, - '2': self.com, - }, - part='EVQ-P7C01P' - ) + self.footprint( + "SW", + "Button_Switch_SMD:SW_SPST_EVQP7C", # 3.5mm x 2.9/3.55mm w/ boss + { + "1": self.sw, + "2": self.com, + }, + part="EVQ-P7C01P", + ) class KailhSocket(MechanicalKeyswitch, FootprintBlock): - """Kailh mechanical keyboard hotswap socket. - Requires an external library, Keyswitch Kicad Library, can be installed from the - KiCad Plugin and Content Manager, or from GitHub https://github.com/perigoso/keyswitch-kicad-library - Even after the content manager install, it must also be manually added to the footprint table: - Name: Switch_Keyboard_Hotswap_Kailh - Location: ${KICAD6_3RD_PARTY}/footprints/com_github_perigoso_keyswitch-kicad-library/Switch_Keyboard_Hotswap_Kailh.pretty - """ - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'SW', 'Switch_Keyboard_Hotswap_Kailh:SW_Hotswap_Kailh_MX', - { - '1': self.sw, - '2': self.com, - }, - mfr='Kailh', part='PG151101S11', - datasheet='https://github.com/keyboardio/keyswitch_documentation/raw/master/datasheets/Kailh/PG151101S11-MX-Socket.pdf', - ) + """Kailh mechanical keyboard hotswap socket. + Requires an external library, Keyswitch Kicad Library, can be installed from the + KiCad Plugin and Content Manager, or from GitHub https://github.com/perigoso/keyswitch-kicad-library + Even after the content manager install, it must also be manually added to the footprint table: + Name: Switch_Keyboard_Hotswap_Kailh + Location: ${KICAD6_3RD_PARTY}/footprints/com_github_perigoso_keyswitch-kicad-library/Switch_Keyboard_Hotswap_Kailh.pretty + """ + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "SW", + "Switch_Keyboard_Hotswap_Kailh:SW_Hotswap_Kailh_MX", + { + "1": self.sw, + "2": self.com, + }, + mfr="Kailh", + part="PG151101S11", + datasheet="https://github.com/keyboardio/keyswitch_documentation/raw/master/datasheets/Kailh/PG151101S11-MX-Socket.pdf", + ) diff --git a/edg/parts/TestPoint_Keystone.py b/edg/parts/TestPoint_Keystone.py index 9dca08f45..607998540 100644 --- a/edg/parts/TestPoint_Keystone.py +++ b/edg/parts/TestPoint_Keystone.py @@ -5,54 +5,63 @@ class Keystone5015(TestPoint, FootprintBlock, JlcPart): - """Keystone 5015 / 5017 (difference in p/n is only from packaging) SMD test point""" - @override - def contents(self) -> None: - super().contents() - self.assign(self.lcsc_part, 'C238130') - self.assign(self.actual_basic_part, False) - self.footprint( - 'TP', 'TestPoint:TestPoint_Keystone_5015_Micro-Minature', - { - '1': self.io, - }, - value=self.tp_name, - mfr='Keystone', part='5015', - datasheet='https://www.keyelco.com/userAssets/file/M65p55.pdf' - ) + """Keystone 5015 / 5017 (difference in p/n is only from packaging) SMD test point""" + + @override + def contents(self) -> None: + super().contents() + self.assign(self.lcsc_part, "C238130") + self.assign(self.actual_basic_part, False) + self.footprint( + "TP", + "TestPoint:TestPoint_Keystone_5015_Micro-Minature", + { + "1": self.io, + }, + value=self.tp_name, + mfr="Keystone", + part="5015", + datasheet="https://www.keyelco.com/userAssets/file/M65p55.pdf", + ) class CompactKeystone5015(TestPoint, FootprintBlock, JlcPart): - """Keystone 5015 / 5017 but with an experimental compact footprint""" - @override - def contents(self) -> None: - super().contents() - self.assign(self.lcsc_part, 'C2906768') - self.assign(self.actual_basic_part, False) - self.footprint( - 'TP', 'edg:TestPoint_TE_RCT_0805', - { - '1': self.io, - }, - value=self.tp_name, - mfr='Keystone', part='5015', - datasheet='https://www.keyelco.com/userAssets/file/M65p55.pdf' - ) + """Keystone 5015 / 5017 but with an experimental compact footprint""" + + @override + def contents(self) -> None: + super().contents() + self.assign(self.lcsc_part, "C2906768") + self.assign(self.actual_basic_part, False) + self.footprint( + "TP", + "edg:TestPoint_TE_RCT_0805", + { + "1": self.io, + }, + value=self.tp_name, + mfr="Keystone", + part="5015", + datasheet="https://www.keyelco.com/userAssets/file/M65p55.pdf", + ) class Keystone5000(TestPoint, FootprintBlock, JlcPart): - """Keystone 5000-5004 series PTH test mini points""" - @override - def contents(self) -> None: - super().contents() - self.assign(self.lcsc_part, 'C238122') - self.assign(self.actual_basic_part, False) - self.footprint( - 'TP', 'TestPoint:TestPoint_Keystone_5000-5004_Miniature', - { - '1': self.io, - }, - value=self.tp_name, - mfr='Keystone', part='5001', - datasheet='https://www.keyelco.com/userAssets/file/M65p56.pdf' - ) + """Keystone 5000-5004 series PTH test mini points""" + + @override + def contents(self) -> None: + super().contents() + self.assign(self.lcsc_part, "C238122") + self.assign(self.actual_basic_part, False) + self.footprint( + "TP", + "TestPoint:TestPoint_Keystone_5000-5004_Miniature", + { + "1": self.io, + }, + value=self.tp_name, + mfr="Keystone", + part="5001", + datasheet="https://www.keyelco.com/userAssets/file/M65p56.pdf", + ) diff --git a/edg/parts/TestPoint_Rc.py b/edg/parts/TestPoint_Rc.py index 6d0338088..f9281bfa9 100644 --- a/edg/parts/TestPoint_Rc.py +++ b/edg/parts/TestPoint_Rc.py @@ -4,33 +4,36 @@ class TeRc(TestPoint, FootprintBlock, GeneratorBlock): - """TE Type RC 0603/0805/1206 SMD test point""" - _PART_TABLE = { # size designator -> part number, footprint - '0603': ('RCU-0C', 'edg:TestPoint_TE_RCU_0603'), # pretty annoying to solder actually - '0805': ('RCT-0C', 'edg:TestPoint_TE_RCT_0805'), - '1206': ('RCS-0C', 'edg:TestPoint_TE_RCS_RCW_1206'), - } + """TE Type RC 0603/0805/1206 SMD test point""" - def __init__(self, size: StringLike = '0805'): - super().__init__() - self.size = self.ArgParameter(size) - self.generator_param(self.size) + _PART_TABLE = { # size designator -> part number, footprint + "0603": ("RCU-0C", "edg:TestPoint_TE_RCU_0603"), # pretty annoying to solder actually + "0805": ("RCT-0C", "edg:TestPoint_TE_RCT_0805"), + "1206": ("RCS-0C", "edg:TestPoint_TE_RCS_RCW_1206"), + } - @override - def generate(self) -> None: - super().generate() - if self.get(self.size) not in self._PART_TABLE: - allowed_sizes = ', '.join(self._PART_TABLE.keys()) - self.require(False, f"invalid size designator '{self.get(self.size)}', must be in ({allowed_sizes})") - return + def __init__(self, size: StringLike = "0805"): + super().__init__() + self.size = self.ArgParameter(size) + self.generator_param(self.size) - part, footprint = self._PART_TABLE[self.get(self.size)] - self.footprint( - 'TP', footprint, - { - '1': self.io, - }, - value=self.tp_name, - mfr='TE Connectivity', part=part, - datasheet='https://www.te.com/commerce/DocumentDelivery/DDEController?Action=srchrtrv&DocNm=1773266&DocType=DS&DocLang=English' - ) + @override + def generate(self) -> None: + super().generate() + if self.get(self.size) not in self._PART_TABLE: + allowed_sizes = ", ".join(self._PART_TABLE.keys()) + self.require(False, f"invalid size designator '{self.get(self.size)}', must be in ({allowed_sizes})") + return + + part, footprint = self._PART_TABLE[self.get(self.size)] + self.footprint( + "TP", + footprint, + { + "1": self.io, + }, + value=self.tp_name, + mfr="TE Connectivity", + part=part, + datasheet="https://www.te.com/commerce/DocumentDelivery/DDEController?Action=srchrtrv&DocNm=1773266&DocType=DS&DocLang=English", + ) diff --git a/edg/parts/ThermalSensor_FlirLepton.py b/edg/parts/ThermalSensor_FlirLepton.py index 6641fbb16..a4bf03490 100644 --- a/edg/parts/ThermalSensor_FlirLepton.py +++ b/edg/parts/ThermalSensor_FlirLepton.py @@ -8,29 +8,33 @@ class FlirLepton_Device(InternalSubcircuit, FootprintBlock, JlcPart): def __init__(self) -> None: super().__init__() self.gnd = self.Port(Ground()) - self.vddc = self.Port(VoltageSink.from_gnd( - self.gnd, - voltage_limits=(1.14, 1.26)*Volt, # 50mVpp max ripple - current_draw=(76, 110)*mAmp # no sleep current specified - )) - self.vdd = self.Port(VoltageSink.from_gnd( - self.gnd, - voltage_limits=(2.72, 2.88)*Volt, # 30mVpp max ripple, 4.8V abs max - current_draw=(12, 16)*mAmp # no sleep current specified - )) - self.vddio = self.Port(VoltageSink.from_gnd( # IO ring and shutter assembly - self.gnd, - voltage_limits=(2.8, 3.1)*Volt, # 50mVpp max ripple, 4.8V abs max - current_draw=(1, 310)*mAmp # min to max during FFC - )) - dio_model = DigitalBidir.from_supply( - self.gnd, self.vddio, - voltage_limit_tolerance=(0, 0.6)*Volt) + self.vddc = self.Port( + VoltageSink.from_gnd( + self.gnd, + voltage_limits=(1.14, 1.26) * Volt, # 50mVpp max ripple + current_draw=(76, 110) * mAmp, # no sleep current specified + ) + ) + self.vdd = self.Port( + VoltageSink.from_gnd( + self.gnd, + voltage_limits=(2.72, 2.88) * Volt, # 30mVpp max ripple, 4.8V abs max + current_draw=(12, 16) * mAmp, # no sleep current specified + ) + ) + self.vddio = self.Port( + VoltageSink.from_gnd( # IO ring and shutter assembly + self.gnd, + voltage_limits=(2.8, 3.1) * Volt, # 50mVpp max ripple, 4.8V abs max + current_draw=(1, 310) * mAmp, # min to max during FFC + ) + ) + dio_model = DigitalBidir.from_supply(self.gnd, self.vddio, voltage_limit_tolerance=(0, 0.6) * Volt) self.master_clk = self.Port(DigitalSink.from_bidir(dio_model)) # 25MHz clock - self.spi = self.Port(SpiPeripheral(dio_model, (0, 20)*MHertz)) + self.spi = self.Port(SpiPeripheral(dio_model, (0, 20) * MHertz)) self.cs = self.Port(DigitalSink.from_bidir(dio_model)) - self.cci = self.Port(I2cTarget(dio_model, [0x2a])) # frequency up to 1MHz + self.cci = self.Port(I2cTarget(dio_model, [0x2A])) # frequency up to 1MHz self.reset_l = self.Port(DigitalSink.from_bidir(dio_model)) self.pwr_dwn_l = self.Port(DigitalSink.from_bidir(dio_model)) @@ -41,47 +45,48 @@ def contents(self) -> None: super().contents() self.footprint( - 'U', 'edg:Molex_1050281001', + "U", + "edg:Molex_1050281001", { - '1': self.gnd, - '6': self.gnd, - '8': self.gnd, - '9': self.gnd, - '10': self.gnd, - '15': self.gnd, - '18': self.gnd, - '20': self.gnd, - '25': self.gnd, - '27': self.gnd, - '30': self.gnd, - '33': self.gnd, # socket shield - - '2': self.vsync, # aka GPIO3 + "1": self.gnd, + "6": self.gnd, + "8": self.gnd, + "9": self.gnd, + "10": self.gnd, + "15": self.gnd, + "18": self.gnd, + "20": self.gnd, + "25": self.gnd, + "27": self.gnd, + "30": self.gnd, + "33": self.gnd, # socket shield + "2": self.vsync, # aka GPIO3 # '3': GPIO2, reserved, "should not be connected" # '4': GPIO1, reserved, "should not be connected" # '5': GPIO0, reserved, "should not be connected" - '7': self.vddc, - '11': self.spi.mosi, - '12': self.spi.miso, - '13': self.spi.sck, - '14': self.cs, - '16': self.vddio, + "7": self.vddc, + "11": self.spi.mosi, + "12": self.spi.miso, + "13": self.spi.sck, + "14": self.cs, + "16": self.vddio, # '17': self.vprog, # unused - '19': self.vdd, - '21': self.cci.scl, - '22': self.cci.sda, - '23': self.pwr_dwn_l, - '24': self.reset_l, - '26': self.master_clk, + "19": self.vdd, + "21": self.cci.scl, + "22": self.cci.sda, + "23": self.pwr_dwn_l, + "24": self.reset_l, + "26": self.master_clk, # '28': reserved # '29': reserved # '31': reserved # '32': reserved }, - mfr='Molex', part='1050281001', - datasheet='https://www.molex.com/pdm_docs/ps/PS-105028-101.pdf' + mfr="Molex", + part="1050281001", + datasheet="https://www.molex.com/pdm_docs/ps/PS-105028-101.pdf", ) - self.assign(self.lcsc_part, 'C585422') + self.assign(self.lcsc_part, "C585422") self.assign(self.actual_basic_part, False) @@ -90,11 +95,14 @@ class FlirLepton(Sensor, Resettable, Block): <50mK (35mK typical) NETD. Only the part number for the socket is generated, the sensor (a $100+ part) must be purchased separately. """ + def __init__(self) -> None: super().__init__() self.ic = self.Block(FlirLepton_Device()) self.gnd = self.Export(self.ic.gnd, [Common]) - self.pwr_io = self.Export(self.ic.vddio, doc="3.0v IO voltage including shutter, IOs are 3.3v compatible from +0.6v tolerance rating") + self.pwr_io = self.Export( + self.ic.vddio, doc="3.0v IO voltage including shutter, IOs are 3.3v compatible from +0.6v tolerance rating" + ) self.pwr = self.Export(self.ic.vdd, doc="2.8v") self.pwr_core = self.Export(self.ic.vddc, doc="1.2v core voltage") @@ -112,12 +120,12 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() - self.vddc_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vddc) - self.vddio_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vddio) - self.vdd_cap = self.Block(DecouplingCapacitor(100*nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + self.vddc_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vddc) + self.vddio_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vddio) + self.vdd_cap = self.Block(DecouplingCapacitor(100 * nFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) with self.implicit_connect( - ImplicitConnect(self.pwr_io, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.pwr_io, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: - (self.mclk, ), _ = self.chain(imp.Block(Oscillator((24.975, 25.025)*MHertz)), self.ic.master_clk) + (self.mclk,), _ = self.chain(imp.Block(Oscillator((24.975, 25.025) * MHertz)), self.ic.master_clk) diff --git a/edg/parts/UsbInterface_Ft232h.py b/edg/parts/UsbInterface_Ft232h.py index 3083afdd0..c4633483d 100644 --- a/edg/parts/UsbInterface_Ft232h.py +++ b/edg/parts/UsbInterface_Ft232h.py @@ -6,252 +6,283 @@ class Ft232hl_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground()) - self.vregin = self.Port(VoltageSink( # bus-powered: connect to VBUS - voltage_limits=(3.6, 5.5)*Volt, # Table 5.2 for VREGIN=5v - current_draw=(54, 54)*mAmp # Table 5.2 typ for VREGIN=5v - )) - self.vccd = self.Port(VoltageSource( # is an output since VREGIN is +5v - voltage_out=(3.0, 3.6)*Volt, # not specified, inferred from limits of connected inputs - current_limits=(0, 62)*mAmp, # not specified, inferred from draw of connected inputs + EEPROM - )) - self.vcccore = self.Port(VoltageSource( # decouple with 0.1uF cap, recommended 1.62-1.98v - voltage_out=(1.62, 1.98)*Volt, # assumed from Vcore limits - current_limits=(0, 0)*mAmp, # not specified, external sourcing disallowed - )) - self.vcca = self.Port(VoltageSource( # 1.8v output, decouple with 0.1uF cap - voltage_out=(1.62, 1.98)*Volt, # assumed from Vcore limits - )) - - self.vphy = self.Port(VoltageSink( # connect to 3v3 through 600R 0.5A ferrite - voltage_limits=(3.0, 3.6)*Volt, # Table 5.4 - current_draw=(0.010, 60)*mAmp, # Table 5.4 suspend typ to operating max - )) - self.vpll = self.Port(VoltageSink( # connect to 3v3 through 600R 0.5A ferrite - voltage_limits=(3.0, 3.6)*Volt, # Table 5.4 - both VPHY and VPLL - )) - self.vccio = self.Port(VoltageSink( - voltage_limits=(2.97, 3.63)*Volt, # Table 5.2, "cells are 5v tolerant" - # current not specified - )) - - self.osc = self.Port(CrystalDriver(frequency_limits=12*MHertz(tol=30e-6), - voltage_out=self.vccd.link().voltage)) # assumed - self.ref = self.Port(Passive()) # connect 12k 1% resistor to GND - self.usb = self.Port(UsbDevicePort()) - - self._dio_model = DigitalBidir.from_supply( # except USB pins which are not 5v tolerant - self.gnd, self.vccio, - current_limits=(-16, 16)*mAmp, # Table 5.1, assumed bidirectional - voltage_limit_abs=(-0.3, 5.8)*Volt, # Table 5.1 high impedance bidirectional - input_threshold_abs=(0.8, 2.0)*Volt, # Table 5.3 - ) - self._din_model = DigitalSink.from_bidir(self._dio_model) - self._dout_model = DigitalSource.from_bidir(self._dio_model) - - self.nreset = self.Port(self._din_model, optional=True) - - self.eecs = self.Port(self._dout_model, optional=True) - self.eeclk = self.Port(self._dout_model, optional=True) - self.eedata = self.Port(self._dio_model, optional=True) - - # TODO these should be aliased to the supported serial buses - self.adbus = self.Port(Vector(DigitalBidir.empty())) - self.acbus = self.Port(Vector(DigitalBidir.empty())) - - @override - def contents(self) -> None: - super().contents() - - for i in range(8): - self.adbus.append_elt(self._dio_model, str(i)) - for i in range(10): # these are GPIOs - self.acbus.append_elt(self._dio_model, str(i)) - - self.footprint( # pinning in order of table in Section 3.3 - 'U', 'Package_QFP:LQFP-48_7x7mm_P0.5mm', - { - '40': self.vregin, - '37': self.vcca, - '38': self.vcccore, - '39': self.vccd, - '12': self.vccio, - '24': self.vccio, - '46': self.vccio, - '8': self.vpll, - '3': self.vphy, - '4': self.gnd, # AGND - '9': self.gnd, # AGND - '41': self.gnd, # AGND - '10': self.gnd, - '11': self.gnd, - '22': self.gnd, - '23': self.gnd, - '35': self.gnd, - '36': self.gnd, - '47': self.gnd, - '48': self.gnd, - - '1': self.osc.xtal_in, - '2': self.osc.xtal_out, - '5': self.ref, - '6': self.usb.dm, - '7': self.usb.dp, - '42': self.gnd, # TEST - '34': self.nreset, # active-low reset - - '45': self.eecs, - '44': self.eeclk, - '43': self.eedata, - - '13': self.adbus['0'], - '14': self.adbus['1'], - '15': self.adbus['2'], - '16': self.adbus['3'], - '17': self.adbus['4'], - '18': self.adbus['5'], - '19': self.adbus['6'], - '20': self.adbus['7'], - - '21': self.acbus['0'], - '25': self.acbus['1'], - '26': self.acbus['2'], - '27': self.acbus['3'], - '28': self.acbus['4'], - '29': self.acbus['5'], - '30': self.acbus['6'], - '31': self.acbus['7'], # aka #PWRSAV - '32': self.acbus['8'], - '33': self.acbus['9'], - }, - mfr='FTDI, Future Technology Devices International Ltd', part='FT232HL', - datasheet='https://ftdichip.com/wp-content/uploads/2020/07/DS_FT232H.pdf' - ) - self.assign(self.lcsc_part, "C51997") # FT232HL-REEL - self.assign(self.actual_basic_part, False) + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground()) + self.vregin = self.Port( + VoltageSink( # bus-powered: connect to VBUS + voltage_limits=(3.6, 5.5) * Volt, # Table 5.2 for VREGIN=5v + current_draw=(54, 54) * mAmp, # Table 5.2 typ for VREGIN=5v + ) + ) + self.vccd = self.Port( + VoltageSource( # is an output since VREGIN is +5v + voltage_out=(3.0, 3.6) * Volt, # not specified, inferred from limits of connected inputs + current_limits=(0, 62) * mAmp, # not specified, inferred from draw of connected inputs + EEPROM + ) + ) + self.vcccore = self.Port( + VoltageSource( # decouple with 0.1uF cap, recommended 1.62-1.98v + voltage_out=(1.62, 1.98) * Volt, # assumed from Vcore limits + current_limits=(0, 0) * mAmp, # not specified, external sourcing disallowed + ) + ) + self.vcca = self.Port( + VoltageSource( # 1.8v output, decouple with 0.1uF cap + voltage_out=(1.62, 1.98) * Volt, # assumed from Vcore limits + ) + ) + + self.vphy = self.Port( + VoltageSink( # connect to 3v3 through 600R 0.5A ferrite + voltage_limits=(3.0, 3.6) * Volt, # Table 5.4 + current_draw=(0.010, 60) * mAmp, # Table 5.4 suspend typ to operating max + ) + ) + self.vpll = self.Port( + VoltageSink( # connect to 3v3 through 600R 0.5A ferrite + voltage_limits=(3.0, 3.6) * Volt, # Table 5.4 - both VPHY and VPLL + ) + ) + self.vccio = self.Port( + VoltageSink( + voltage_limits=(2.97, 3.63) * Volt, # Table 5.2, "cells are 5v tolerant" + # current not specified + ) + ) + + self.osc = self.Port( + CrystalDriver(frequency_limits=12 * MHertz(tol=30e-6), voltage_out=self.vccd.link().voltage) + ) # assumed + self.ref = self.Port(Passive()) # connect 12k 1% resistor to GND + self.usb = self.Port(UsbDevicePort()) + + self._dio_model = DigitalBidir.from_supply( # except USB pins which are not 5v tolerant + self.gnd, + self.vccio, + current_limits=(-16, 16) * mAmp, # Table 5.1, assumed bidirectional + voltage_limit_abs=(-0.3, 5.8) * Volt, # Table 5.1 high impedance bidirectional + input_threshold_abs=(0.8, 2.0) * Volt, # Table 5.3 + ) + self._din_model = DigitalSink.from_bidir(self._dio_model) + self._dout_model = DigitalSource.from_bidir(self._dio_model) + + self.nreset = self.Port(self._din_model, optional=True) + + self.eecs = self.Port(self._dout_model, optional=True) + self.eeclk = self.Port(self._dout_model, optional=True) + self.eedata = self.Port(self._dio_model, optional=True) + + # TODO these should be aliased to the supported serial buses + self.adbus = self.Port(Vector(DigitalBidir.empty())) + self.acbus = self.Port(Vector(DigitalBidir.empty())) + + @override + def contents(self) -> None: + super().contents() + + for i in range(8): + self.adbus.append_elt(self._dio_model, str(i)) + for i in range(10): # these are GPIOs + self.acbus.append_elt(self._dio_model, str(i)) + + self.footprint( # pinning in order of table in Section 3.3 + "U", + "Package_QFP:LQFP-48_7x7mm_P0.5mm", + { + "40": self.vregin, + "37": self.vcca, + "38": self.vcccore, + "39": self.vccd, + "12": self.vccio, + "24": self.vccio, + "46": self.vccio, + "8": self.vpll, + "3": self.vphy, + "4": self.gnd, # AGND + "9": self.gnd, # AGND + "41": self.gnd, # AGND + "10": self.gnd, + "11": self.gnd, + "22": self.gnd, + "23": self.gnd, + "35": self.gnd, + "36": self.gnd, + "47": self.gnd, + "48": self.gnd, + "1": self.osc.xtal_in, + "2": self.osc.xtal_out, + "5": self.ref, + "6": self.usb.dm, + "7": self.usb.dp, + "42": self.gnd, # TEST + "34": self.nreset, # active-low reset + "45": self.eecs, + "44": self.eeclk, + "43": self.eedata, + "13": self.adbus["0"], + "14": self.adbus["1"], + "15": self.adbus["2"], + "16": self.adbus["3"], + "17": self.adbus["4"], + "18": self.adbus["5"], + "19": self.adbus["6"], + "20": self.adbus["7"], + "21": self.acbus["0"], + "25": self.acbus["1"], + "26": self.acbus["2"], + "27": self.acbus["3"], + "28": self.acbus["4"], + "29": self.acbus["5"], + "30": self.acbus["6"], + "31": self.acbus["7"], # aka #PWRSAV + "32": self.acbus["8"], + "33": self.acbus["9"], + }, + mfr="FTDI, Future Technology Devices International Ltd", + part="FT232HL", + datasheet="https://ftdichip.com/wp-content/uploads/2020/07/DS_FT232H.pdf", + ) + self.assign(self.lcsc_part, "C51997") # FT232HL-REEL + self.assign(self.actual_basic_part, False) class Ft232EepromDriver(InternalSubcircuit, Block): - """Adapts the EECLK and EEDATA pins of the FT232 to the SPI of the EEPROM""" - def __init__(self) -> None: - super().__init__() - self.pwr = self.Port(VoltageSink.empty()) - self.eeclk = self.Port(DigitalSink.empty()) - self.eedata = self.Port(DigitalBidir.empty()) - self.spi = self.Port(SpiController.empty()) - - @override - def contents(self) -> None: - super().contents() - self.connect(self.eeclk, self.spi.sck) - self.connect(self.eedata, self.spi.mosi) - self.do_pull = self.Block(PullupResistor(10*kOhm(tol=0.05))).connected(self.pwr, self.spi.miso) - self.do_res = self.Block(Resistor(2*kOhm(tol=0.05))) - self.connect(self.spi.miso, self.do_res.a.adapt_to(DigitalSink())) # sink side port is ideal - self.connect(self.eedata, self.do_res.b.adapt_to(DigitalSource( - voltage_out=self.spi.miso.link().voltage, - output_thresholds=self.spi.miso.link().output_thresholds - ))) + """Adapts the EECLK and EEDATA pins of the FT232 to the SPI of the EEPROM""" + + def __init__(self) -> None: + super().__init__() + self.pwr = self.Port(VoltageSink.empty()) + self.eeclk = self.Port(DigitalSink.empty()) + self.eedata = self.Port(DigitalBidir.empty()) + self.spi = self.Port(SpiController.empty()) + + @override + def contents(self) -> None: + super().contents() + self.connect(self.eeclk, self.spi.sck) + self.connect(self.eedata, self.spi.mosi) + self.do_pull = self.Block(PullupResistor(10 * kOhm(tol=0.05))).connected(self.pwr, self.spi.miso) + self.do_res = self.Block(Resistor(2 * kOhm(tol=0.05))) + self.connect(self.spi.miso, self.do_res.a.adapt_to(DigitalSink())) # sink side port is ideal + self.connect( + self.eedata, + self.do_res.b.adapt_to( + DigitalSource( + voltage_out=self.spi.miso.link().voltage, output_thresholds=self.spi.miso.link().output_thresholds + ) + ), + ) class Ft232hl(Interface, GeneratorBlock): - """USB multiprotocol converter""" - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Ft232hl_Device()) - # TODO connect to 3.3v from ferrite - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - - self.usb = self.Export(self.ic.usb) - - # connect one of UART, MPSSE, or ADBUS pins - self.uart = self.Port(UartPort.empty(), optional=True) - self.mpsse = self.Port(SpiController.empty(), optional=True) - self.mpsse_cs = self.Port(DigitalSource.empty(), optional=True) - - self.adbus = self.Port(Vector(DigitalBidir.empty())) - self.acbus = self.Export(self.ic.acbus) - - # the generator is needed to mux the shared MPSSE and ADBUS pins - self.generator_param(self.uart.is_connected(), self.mpsse.is_connected(), self.mpsse_cs.is_connected(), - self.adbus.requested()) - - @override - def contents(self) -> None: - # connections from Figure 6.1, bus powered configuration - self.vbus_fb = self.Block(SeriesPowerFerriteBead(hf_impedance=(600*Ohm(tol=0.25)))) \ - .connected(self.pwr, self.ic.vregin) - - cap_model = DecouplingCapacitor(0.1*uFarad(tol=0.2)) - self.vregin_cap0 = self.Block(DecouplingCapacitor(4.7*uFarad(tol=0.2))).connected(self.gnd, self.ic.vregin) - self.vregin_cap1 = self.Block(cap_model).connected(self.gnd, self.ic.vregin) - - self.vphy_fb = self.Block(SeriesPowerFerriteBead(hf_impedance=(600*Ohm(tol=0.25)))) \ - .connected(self.ic.vccd, self.ic.vphy) - self.vphy_cap = self.Block(cap_model).connected(self.gnd, self.ic.vphy) - - self.vpll_fb = self.Block(SeriesPowerFerriteBead(hf_impedance=(600*Ohm(tol=0.25)))) \ - .connected(self.ic.vccd, self.ic.vpll) - self.vpll_cap = self.Block(cap_model).connected(self.gnd, self.ic.vpll) - - self.vcccore_cap = self.Block(cap_model).connected(self.gnd, self.ic.vcccore) - self.vcca_cap = self.Block(cap_model).connected(self.gnd, self.ic.vcca) - - self.vccd_cap = self.Block(cap_model).connected(self.gnd, self.ic.vccd) - - self.connect(self.ic.vccd, self.ic.vccio) - self.vccio_cap0 = self.Block(cap_model).connected(self.gnd, self.ic.vccio) - self.vccio_cap1 = self.Block(cap_model).connected(self.gnd, self.ic.vccio) - self.vccio_cap2 = self.Block(cap_model).connected(self.gnd, self.ic.vccio) - - self.ref_res = self.Block(Resistor(12*kOhm(tol=0.01))) - self.connect(self.ref_res.a, self.ic.ref) - self.connect(self.ref_res.b.adapt_to(Ground()), self.gnd) - - self.connect(self.ic.vccd.as_digital_source(), self.ic.nreset) # in concept driven by VccIO - - self.crystal = self.Block(OscillatorReference(frequency=12*MHertz(tol=30e-6))) - self.connect(self.crystal.gnd, self.gnd) - self.connect(self.crystal.crystal, self.ic.osc) - - # optional EEPROM - self.eeprom = self.Block(E93Lc_B(Range.exact(2 * 1024))) - self.connect(self.eeprom.gnd, self.gnd) - self.connect(self.eeprom.pwr, self.ic.vccio) - self.connect(self.eeprom.cs, self.ic.eecs) - self.eeprom_spi = self.Block(Ft232EepromDriver()) - self.connect(self.eeprom_spi.pwr, self.ic.vccio) - self.connect(self.eeprom_spi.eeclk, self.ic.eeclk) - self.connect(self.eeprom_spi.eedata, self.ic.eedata) - self.connect(self.eeprom_spi.spi, self.eeprom.spi) - - @override - def generate(self) -> None: - # make connections and pin mutual exclusion constraints - if self.get(self.uart.is_connected()): - self.connect(self.uart.tx, self.ic.adbus.request('0')) - self.connect(self.uart.rx, self.ic.adbus.request('1')) - self.require(self.uart.is_connected().implies(~self.mpsse.is_connected())) - self.require(self.uart.is_connected().implies('0' not in self.get(self.adbus.requested()) and - '1' not in self.get(self.adbus.requested()))) - - if self.get(self.mpsse.is_connected()): - self.connect(self.mpsse.sck, self.ic.adbus.request('0')) - self.connect(self.mpsse.mosi, self.ic.adbus.request('1')) - self.connect(self.mpsse.miso, self.ic.adbus.request('2')) - if self.get(self.mpsse_cs.is_connected()): - self.connect(self.mpsse_cs, self.ic.adbus.request('3')) - # UART mutual exclusion already handled above - self.require(self.mpsse.is_connected().implies('0' not in self.get(self.adbus.requested()) and - '1' not in self.get(self.adbus.requested()) and - '2' not in self.get(self.adbus.requested()))) - self.require(self.mpsse_cs.is_connected().implies('3' not in self.get(self.adbus.requested()))) - - # require something to be connected - self.require(self.uart.is_connected() | self.mpsse.is_connected() | bool(self.get(self.adbus.requested()))) - - for elt in self.get(self.adbus.requested()): - self.connect(self.adbus.append_elt(DigitalBidir.empty(), elt), self.ic.adbus.request(elt)) + """USB multiprotocol converter""" + + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Ft232hl_Device()) + # TODO connect to 3.3v from ferrite + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + + self.usb = self.Export(self.ic.usb) + + # connect one of UART, MPSSE, or ADBUS pins + self.uart = self.Port(UartPort.empty(), optional=True) + self.mpsse = self.Port(SpiController.empty(), optional=True) + self.mpsse_cs = self.Port(DigitalSource.empty(), optional=True) + + self.adbus = self.Port(Vector(DigitalBidir.empty())) + self.acbus = self.Export(self.ic.acbus) + + # the generator is needed to mux the shared MPSSE and ADBUS pins + self.generator_param( + self.uart.is_connected(), self.mpsse.is_connected(), self.mpsse_cs.is_connected(), self.adbus.requested() + ) + + @override + def contents(self) -> None: + # connections from Figure 6.1, bus powered configuration + self.vbus_fb = self.Block(SeriesPowerFerriteBead(hf_impedance=(600 * Ohm(tol=0.25)))).connected( + self.pwr, self.ic.vregin + ) + + cap_model = DecouplingCapacitor(0.1 * uFarad(tol=0.2)) + self.vregin_cap0 = self.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(self.gnd, self.ic.vregin) + self.vregin_cap1 = self.Block(cap_model).connected(self.gnd, self.ic.vregin) + + self.vphy_fb = self.Block(SeriesPowerFerriteBead(hf_impedance=(600 * Ohm(tol=0.25)))).connected( + self.ic.vccd, self.ic.vphy + ) + self.vphy_cap = self.Block(cap_model).connected(self.gnd, self.ic.vphy) + + self.vpll_fb = self.Block(SeriesPowerFerriteBead(hf_impedance=(600 * Ohm(tol=0.25)))).connected( + self.ic.vccd, self.ic.vpll + ) + self.vpll_cap = self.Block(cap_model).connected(self.gnd, self.ic.vpll) + + self.vcccore_cap = self.Block(cap_model).connected(self.gnd, self.ic.vcccore) + self.vcca_cap = self.Block(cap_model).connected(self.gnd, self.ic.vcca) + + self.vccd_cap = self.Block(cap_model).connected(self.gnd, self.ic.vccd) + + self.connect(self.ic.vccd, self.ic.vccio) + self.vccio_cap0 = self.Block(cap_model).connected(self.gnd, self.ic.vccio) + self.vccio_cap1 = self.Block(cap_model).connected(self.gnd, self.ic.vccio) + self.vccio_cap2 = self.Block(cap_model).connected(self.gnd, self.ic.vccio) + + self.ref_res = self.Block(Resistor(12 * kOhm(tol=0.01))) + self.connect(self.ref_res.a, self.ic.ref) + self.connect(self.ref_res.b.adapt_to(Ground()), self.gnd) + + self.connect(self.ic.vccd.as_digital_source(), self.ic.nreset) # in concept driven by VccIO + + self.crystal = self.Block(OscillatorReference(frequency=12 * MHertz(tol=30e-6))) + self.connect(self.crystal.gnd, self.gnd) + self.connect(self.crystal.crystal, self.ic.osc) + + # optional EEPROM + self.eeprom = self.Block(E93Lc_B(Range.exact(2 * 1024))) + self.connect(self.eeprom.gnd, self.gnd) + self.connect(self.eeprom.pwr, self.ic.vccio) + self.connect(self.eeprom.cs, self.ic.eecs) + self.eeprom_spi = self.Block(Ft232EepromDriver()) + self.connect(self.eeprom_spi.pwr, self.ic.vccio) + self.connect(self.eeprom_spi.eeclk, self.ic.eeclk) + self.connect(self.eeprom_spi.eedata, self.ic.eedata) + self.connect(self.eeprom_spi.spi, self.eeprom.spi) + + @override + def generate(self) -> None: + # make connections and pin mutual exclusion constraints + if self.get(self.uart.is_connected()): + self.connect(self.uart.tx, self.ic.adbus.request("0")) + self.connect(self.uart.rx, self.ic.adbus.request("1")) + self.require(self.uart.is_connected().implies(~self.mpsse.is_connected())) + self.require( + self.uart.is_connected().implies( + "0" not in self.get(self.adbus.requested()) and "1" not in self.get(self.adbus.requested()) + ) + ) + + if self.get(self.mpsse.is_connected()): + self.connect(self.mpsse.sck, self.ic.adbus.request("0")) + self.connect(self.mpsse.mosi, self.ic.adbus.request("1")) + self.connect(self.mpsse.miso, self.ic.adbus.request("2")) + if self.get(self.mpsse_cs.is_connected()): + self.connect(self.mpsse_cs, self.ic.adbus.request("3")) + # UART mutual exclusion already handled above + self.require( + self.mpsse.is_connected().implies( + "0" not in self.get(self.adbus.requested()) + and "1" not in self.get(self.adbus.requested()) + and "2" not in self.get(self.adbus.requested()) + ) + ) + self.require(self.mpsse_cs.is_connected().implies("3" not in self.get(self.adbus.requested()))) + + # require something to be connected + self.require(self.uart.is_connected() | self.mpsse.is_connected() | bool(self.get(self.adbus.requested()))) + + for elt in self.get(self.adbus.requested()): + self.connect(self.adbus.append_elt(DigitalBidir.empty(), elt), self.ic.adbus.request(elt)) diff --git a/edg/parts/UsbPorts.py b/edg/parts/UsbPorts.py index b3a6f35f2..0d46c376c 100644 --- a/edg/parts/UsbPorts.py +++ b/edg/parts/UsbPorts.py @@ -5,251 +5,260 @@ class UsbAReceptacle(UsbHostConnector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.pwr.init_from(VoltageSink( - voltage_limits=self.USB2_VOLTAGE_RANGE, - current_draw=self.USB2_CURRENT_LIMITS - )) - self.gnd.init_from(Ground()) - - self.usb.init_from(UsbDevicePort()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'J', 'Connector_USB:USB_A_Molex_105057_Vertical', - { - '1': self.pwr, - '4': self.gnd, - - '2': self.usb.dm, - '3': self.usb.dp, - - '5': self.gnd, # shield - }, - mfr='Molex', part='105057', - datasheet='https://www.molex.com/pdm_docs/sd/1050570001_sd.pdf' - ) + def __init__(self) -> None: + super().__init__() + self.pwr.init_from(VoltageSink(voltage_limits=self.USB2_VOLTAGE_RANGE, current_draw=self.USB2_CURRENT_LIMITS)) + self.gnd.init_from(Ground()) + + self.usb.init_from(UsbDevicePort()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "J", + "Connector_USB:USB_A_Molex_105057_Vertical", + { + "1": self.pwr, + "4": self.gnd, + "2": self.usb.dm, + "3": self.usb.dp, + "5": self.gnd, # shield + }, + mfr="Molex", + part="105057", + datasheet="https://www.molex.com/pdm_docs/sd/1050570001_sd.pdf", + ) class UsbCReceptacle_Device(InternalSubcircuit, FootprintBlock, JlcPart): - """Raw USB Type-C Receptacle - Pullup capable indicates whether this port (or more accurately, the device on the other side) can pull - up the signal. In UFP (upstream-facing, device) mode the power source should pull up CC.""" - def __init__(self, voltage_out: RangeLike = UsbConnector.USB2_VOLTAGE_RANGE, # allow custom PD voltage and current - current_limits: RangeLike = UsbConnector.USB2_CURRENT_LIMITS, - cc_pullup_capable: BoolLike = False) -> None: - super().__init__() - self.pwr = self.Port(VoltageSource(voltage_out=voltage_out, current_limits=current_limits), optional=True) - self.gnd = self.Port(Ground()) - - self.usb = self.Port(UsbHostPort(), optional=True) - self.shield = self.Port(Passive(), optional=True) - - self.cc = self.Port(UsbCcPort(pullup_capable=cc_pullup_capable), optional=True) - - @override - def contents(self) -> None: - super().contents() - - self.assign(self.lcsc_part, 'C165948') # note, many other pin-compatible parts also available - self.assign(self.actual_basic_part, False) - self.footprint( - 'J', 'Connector_USB:USB_C_Receptacle_XKB_U262-16XN-4BVC11', - { - 'A1': self.gnd, - 'B12': self.gnd, - 'A4': self.pwr, - 'B9': self.pwr, - - 'A5': self.cc.cc1, - 'A6': self.usb.dp, - 'A7': self.usb.dm, - # 'A8': sbu1, - - # 'B8': sbu2 - 'B7': self.usb.dm, - 'B6': self.usb.dp, - 'B5': self.cc.cc2, - - 'B4': self.pwr, - 'A9': self.pwr, - 'B1': self.gnd, - 'A12': self.gnd, - - 'S1': self.shield, - }, - mfr='Sparkfun', part='COM-15111', - datasheet='https://cdn.sparkfun.com/assets/8/6/b/4/5/A40-00119-A52-12.pdf' - ) + """Raw USB Type-C Receptacle + Pullup capable indicates whether this port (or more accurately, the device on the other side) can pull + up the signal. In UFP (upstream-facing, device) mode the power source should pull up CC.""" + + def __init__( + self, + voltage_out: RangeLike = UsbConnector.USB2_VOLTAGE_RANGE, # allow custom PD voltage and current + current_limits: RangeLike = UsbConnector.USB2_CURRENT_LIMITS, + cc_pullup_capable: BoolLike = False, + ) -> None: + super().__init__() + self.pwr = self.Port(VoltageSource(voltage_out=voltage_out, current_limits=current_limits), optional=True) + self.gnd = self.Port(Ground()) + + self.usb = self.Port(UsbHostPort(), optional=True) + self.shield = self.Port(Passive(), optional=True) + + self.cc = self.Port(UsbCcPort(pullup_capable=cc_pullup_capable), optional=True) + + @override + def contents(self) -> None: + super().contents() + + self.assign(self.lcsc_part, "C165948") # note, many other pin-compatible parts also available + self.assign(self.actual_basic_part, False) + self.footprint( + "J", + "Connector_USB:USB_C_Receptacle_XKB_U262-16XN-4BVC11", + { + "A1": self.gnd, + "B12": self.gnd, + "A4": self.pwr, + "B9": self.pwr, + "A5": self.cc.cc1, + "A6": self.usb.dp, + "A7": self.usb.dm, + # 'A8': sbu1, + # 'B8': sbu2 + "B7": self.usb.dm, + "B6": self.usb.dp, + "B5": self.cc.cc2, + "B4": self.pwr, + "A9": self.pwr, + "B1": self.gnd, + "A12": self.gnd, + "S1": self.shield, + }, + mfr="Sparkfun", + part="COM-15111", + datasheet="https://cdn.sparkfun.com/assets/8/6/b/4/5/A40-00119-A52-12.pdf", + ) class UsbCReceptacle(UsbDeviceConnector, GeneratorBlock): - """USB Type-C Receptacle that automatically generates the CC resistors if CC is not connected.""" - def __init__(self, voltage_out: RangeLike = UsbConnector.USB2_VOLTAGE_RANGE, # allow custom PD voltage and current - current_limits: RangeLike = UsbConnector.USB2_CURRENT_LIMITS) -> None: - super().__init__() - - self.conn = self.Block(UsbCReceptacle_Device(voltage_out=voltage_out, current_limits=current_limits)) - self.connect(self.pwr, self.conn.pwr) - self.connect(self.gnd, self.conn.gnd) - self.connect(self.usb, self.conn.usb) - self.cc = self.Port(UsbCcPort.empty(), optional=True) # external connectivity defines the circuit - - self.generator_param(self.pwr.is_connected(), self.cc.is_connected()) - - @override - def generate(self) -> None: - super().generate() - if self.get(self.cc.is_connected()): # if CC externally connected, connect directly to USB port - self.connect(self.cc, self.conn.cc) - self.require(self.cc.is_connected().implies(self.pwr.is_connected()), - "USB power not used when CC connected") - elif self.get(self.pwr.is_connected()): # otherwise generate the pulldown resistors for USB2 mode - (self.cc_pull, ), _ = self.chain(self.conn.cc, self.Block(UsbCcPulldownResistor())) - self.connect(self.cc_pull.gnd, self.gnd) - self.require(self.pwr.voltage_out == UsbConnector.USB2_VOLTAGE_RANGE, - "when CC not connected, port restricted to USB 2.0 voltage") - # note that the DFP (power source) can provide the max current, however the UFP (device) - # should sense the voltage at CC to determine the amount of current allowed - - # TODO there does not seem to be full agreement on what to do with the shield pin, we arbitrarily ground it - self.connect(self.gnd, self.conn.shield.adapt_to(Ground())) + """USB Type-C Receptacle that automatically generates the CC resistors if CC is not connected.""" + + def __init__( + self, + voltage_out: RangeLike = UsbConnector.USB2_VOLTAGE_RANGE, # allow custom PD voltage and current + current_limits: RangeLike = UsbConnector.USB2_CURRENT_LIMITS, + ) -> None: + super().__init__() + + self.conn = self.Block(UsbCReceptacle_Device(voltage_out=voltage_out, current_limits=current_limits)) + self.connect(self.pwr, self.conn.pwr) + self.connect(self.gnd, self.conn.gnd) + self.connect(self.usb, self.conn.usb) + self.cc = self.Port(UsbCcPort.empty(), optional=True) # external connectivity defines the circuit + + self.generator_param(self.pwr.is_connected(), self.cc.is_connected()) + + @override + def generate(self) -> None: + super().generate() + if self.get(self.cc.is_connected()): # if CC externally connected, connect directly to USB port + self.connect(self.cc, self.conn.cc) + self.require( + self.cc.is_connected().implies(self.pwr.is_connected()), "USB power not used when CC connected" + ) + elif self.get(self.pwr.is_connected()): # otherwise generate the pulldown resistors for USB2 mode + (self.cc_pull,), _ = self.chain(self.conn.cc, self.Block(UsbCcPulldownResistor())) + self.connect(self.cc_pull.gnd, self.gnd) + self.require( + self.pwr.voltage_out == UsbConnector.USB2_VOLTAGE_RANGE, + "when CC not connected, port restricted to USB 2.0 voltage", + ) + # note that the DFP (power source) can provide the max current, however the UFP (device) + # should sense the voltage at CC to determine the amount of current allowed + + # TODO there does not seem to be full agreement on what to do with the shield pin, we arbitrarily ground it + self.connect(self.gnd, self.conn.shield.adapt_to(Ground())) class UsbAPlugPads(UsbDeviceConnector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - @override - def contents(self) -> None: - super().contents() - self.pwr.init_from(VoltageSource( - voltage_out=self.USB2_VOLTAGE_RANGE, - current_limits=self.USB2_CURRENT_LIMITS - )) - self.gnd.init_from(Ground()) - self.usb.init_from(UsbHostPort()) - - self.footprint( - 'J', 'edg:USB_A_Pads', - { - '1': self.pwr, - '2': self.usb.dm, - '3': self.usb.dp, - '4': self.gnd, - }, - part='USB A pads', - ) + def __init__(self) -> None: + super().__init__() + + @override + def contents(self) -> None: + super().contents() + self.pwr.init_from(VoltageSource(voltage_out=self.USB2_VOLTAGE_RANGE, current_limits=self.USB2_CURRENT_LIMITS)) + self.gnd.init_from(Ground()) + self.usb.init_from(UsbHostPort()) + + self.footprint( + "J", + "edg:USB_A_Pads", + { + "1": self.pwr, + "2": self.usb.dm, + "3": self.usb.dp, + "4": self.gnd, + }, + part="USB A pads", + ) class UsbMicroBReceptacle(UsbDeviceConnector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - @override - def contents(self) -> None: - super().contents() - self.pwr.init_from(VoltageSource( - voltage_out=self.USB2_VOLTAGE_RANGE, - current_limits=self.USB2_CURRENT_LIMITS - )) - self.gnd.init_from(Ground()) - self.usb.init_from(UsbHostPort()) - - self.footprint( - 'J', 'Connector_USB:USB_Micro-B_Molex-105017-0001', - { - '1': self.pwr, - '5': self.gnd, - - '2': self.usb.dm, - '3': self.usb.dp, - - # '4': TODO: ID pin - - '6': self.gnd, # shield - }, - mfr='Molex', part='105017-0001', - datasheet='https://www.molex.com/pdm_docs/sd/1050170001_sd.pdf' - ) + def __init__(self) -> None: + super().__init__() + + @override + def contents(self) -> None: + super().contents() + self.pwr.init_from(VoltageSource(voltage_out=self.USB2_VOLTAGE_RANGE, current_limits=self.USB2_CURRENT_LIMITS)) + self.gnd.init_from(Ground()) + self.usb.init_from(UsbHostPort()) + + self.footprint( + "J", + "Connector_USB:USB_Micro-B_Molex-105017-0001", + { + "1": self.pwr, + "5": self.gnd, + "2": self.usb.dm, + "3": self.usb.dp, + # '4': TODO: ID pin + "6": self.gnd, # shield + }, + mfr="Molex", + part="105017-0001", + datasheet="https://www.molex.com/pdm_docs/sd/1050170001_sd.pdf", + ) class UsbCcPulldownResistor(InternalSubcircuit, Block): - """Pull-down resistors on the CC lines for a device to request power from a type-C UFP port, - without needing a USB PD IC.""" - def __init__(self) -> None: - super().__init__() - self.cc = self.Port(UsbCcPort.empty(), [Input]) - self.gnd = self.Port(Ground.empty(), [Common]) + """Pull-down resistors on the CC lines for a device to request power from a type-C UFP port, + without needing a USB PD IC.""" + + def __init__(self) -> None: + super().__init__() + self.cc = self.Port(UsbCcPort.empty(), [Input]) + self.gnd = self.Port(Ground.empty(), [Common]) - @override - def contents(self) -> None: - super().contents() - pdr_model = PulldownResistor(resistance=5.1*kOhm(tol=0.01)) - self.cc1 = self.Block(pdr_model).connected(self.gnd, self.cc.cc1) - self.cc2 = self.Block(pdr_model).connected(self.gnd, self.cc.cc2) + @override + def contents(self) -> None: + super().contents() + pdr_model = PulldownResistor(resistance=5.1 * kOhm(tol=0.01)) + self.cc1 = self.Block(pdr_model).connected(self.gnd, self.cc.cc1) + self.cc2 = self.Block(pdr_model).connected(self.gnd, self.cc.cc2) class Tpd2e009(UsbEsdDiode, FootprintBlock, JlcPart): - @override - def contents(self) -> None: - # Note, also compatible: https://www.diodes.com/assets/Datasheets/DT1452-02SO.pdf - # PESD5V0X1BT,215 (different architecture, but USB listed as application) - super().contents() - self.gnd.init_from(Ground()) - self.usb.init_from(UsbPassivePort()) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', - { - '1': self.usb.dm, - '2': self.usb.dp, - '3': self.gnd, - }, - mfr='Texas Instruments', part='TPD2E009', - datasheet='https://www.ti.com/lit/ds/symlink/tpd2e009.pdf' - ) + @override + def contents(self) -> None: + # Note, also compatible: https://www.diodes.com/assets/Datasheets/DT1452-02SO.pdf + # PESD5V0X1BT,215 (different architecture, but USB listed as application) + super().contents() + self.gnd.init_from(Ground()) + self.usb.init_from(UsbPassivePort()) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23", + { + "1": self.usb.dm, + "2": self.usb.dp, + "3": self.gnd, + }, + mfr="Texas Instruments", + part="TPD2E009", + datasheet="https://www.ti.com/lit/ds/symlink/tpd2e009.pdf", + ) class Pesd5v0x1bt(UsbEsdDiode, FootprintBlock, JlcPart): - """Ultra low capacitance ESD protection diode (0.9pF typ), suitable for USB and GbE""" - @override - def contents(self) -> None: - super().contents() - self.gnd.init_from(Ground()) - self.usb.init_from(UsbPassivePort()) - self.assign(self.lcsc_part, 'C456094') - self.assign(self.actual_basic_part, False) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', - { - '1': self.usb.dm, - '2': self.usb.dp, - '3': self.gnd, - }, - mfr='Nexperia', part='PESD5V0X1BT', - datasheet='https://assets.nexperia.com/documents/data-sheet/PESD5V0X1BT.pdf' - ) + """Ultra low capacitance ESD protection diode (0.9pF typ), suitable for USB and GbE""" + + @override + def contents(self) -> None: + super().contents() + self.gnd.init_from(Ground()) + self.usb.init_from(UsbPassivePort()) + self.assign(self.lcsc_part, "C456094") + self.assign(self.actual_basic_part, False) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23", + { + "1": self.usb.dm, + "2": self.usb.dp, + "3": self.gnd, + }, + mfr="Nexperia", + part="PESD5V0X1BT", + datasheet="https://assets.nexperia.com/documents/data-sheet/PESD5V0X1BT.pdf", + ) class Pgb102st23(UsbEsdDiode, FootprintBlock, JlcPart): - """ESD suppressor, suitable for high speed protocols including USB2.0, 0.12pF typ""" - @override - def contents(self) -> None: - super().contents() - self.gnd.init_from(Ground()) - self.usb.init_from(UsbPassivePort()) - self.assign(self.lcsc_part, 'C126830') - self.assign(self.actual_basic_part, False) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', - { - '1': self.usb.dm, - '2': self.usb.dp, - '3': self.gnd, - }, - mfr='Littelfuse', part='PGB102ST23', - datasheet='https://www.littelfuse.com/~/media/electronics/datasheets/pulseguard_esd_suppressors/littelfuse_pulseguard_pgb1_datasheet.pdf.pdf' - ) + """ESD suppressor, suitable for high speed protocols including USB2.0, 0.12pF typ""" + + @override + def contents(self) -> None: + super().contents() + self.gnd.init_from(Ground()) + self.usb.init_from(UsbPassivePort()) + self.assign(self.lcsc_part, "C126830") + self.assign(self.actual_basic_part, False) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23", + { + "1": self.usb.dm, + "2": self.usb.dp, + "3": self.gnd, + }, + mfr="Littelfuse", + part="PGB102ST23", + datasheet="https://www.littelfuse.com/~/media/electronics/datasheets/pulseguard_esd_suppressors/littelfuse_pulseguard_pgb1_datasheet.pdf.pdf", + ) diff --git a/edg/parts/UsbUart_Cp2102.py b/edg/parts/UsbUart_Cp2102.py index 4232997fa..2f49913d8 100644 --- a/edg/parts/UsbUart_Cp2102.py +++ b/edg/parts/UsbUart_Cp2102.py @@ -5,106 +5,123 @@ class Cp2102_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground()) - self.regin = self.Port(VoltageSink( - voltage_limits=(4.0, 5.25)*Volt, # Table 6 - current_draw=(0.080, 26)*mAmp # TAble 3, suspended typ to normal max - )) - self.vdd = self.Port(VoltageSource( # as input, limits are 3.0-3.6v - voltage_out=(3.0, 3.6)*Volt, # Table 6 - current_limits=(0, 100)*mAmp, # Table 6 note - )) - self.vbus = self.Port(VoltageSink( - voltage_limits=(2.9, 5.8)*Volt, # Table 6 max VBUS threshold Table 2 maximum - # no current draw, is a sense input pin - )) - - self.usb = self.Port(UsbDevicePort()) - - dio_model = DigitalBidir.from_supply( - self.gnd, self.vdd, - current_limits=(-100, 100)*mAmp, # Table 2, assumed sunk is symmetric since no source rating is given - voltage_limit_abs=(-0.3, 5.8)*Volt, - input_threshold_abs=(0.8, 2.0)*Volt, # Table 4 - ) - din_model = DigitalSink.from_bidir(dio_model) - dout_model = DigitalSource.from_bidir(dio_model) - - self.rst = self.Port(din_model, optional=True) - self.suspend = self.Port(dout_model, optional=True) - self.nsuspend = self.Port(dout_model, optional=True) - - self.uart = self.Port(UartPort(dio_model), optional=True) # baud up to 921600bps - self.ri = self.Port(din_model, optional=True) - self.dcd = self.Port(din_model, optional=True) - self.dtr = self.Port(dout_model, optional=True) - self.dsr = self.Port(din_model, optional=True) - self.rts = self.Port(dout_model, optional=True) - self.cts = self.Port(din_model, optional=True) - - self.require(self.uart.is_connected() | self.ri.is_connected() | self.dcd.is_connected() | self.dtr.is_connected() - | self.dsr.is_connected() | self.rts.is_connected() | self.cts.is_connected()) - - self.footprint( - 'U', 'Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.35x3.35mm', - { - '1': self.dcd, - '2': self.ri, - '3': self.gnd, - '4': self.usb.dp, - '5': self.usb.dm, - '6': self.vdd, - '7': self.regin, - '8': self.vbus, - '9': self.rst, - # 10: NC - per Table 9 can be unconnected or tied to Vdd - '11': self.nsuspend, - '12': self.suspend, - # 13-22: NC (18 on CP2109 is VPP) - '23': self.cts, - '24': self.rts, - '25': self.uart.rx, - '26': self.uart.tx, - '27': self.dsr, - '28': self.dtr, - '29': self.gnd, - }, - mfr='Silicon Labs', part='CP2102', - datasheet='https://www.silabs.com/documents/public/data-sheets/CP2102-9.pdf' - ) - self.assign(self.lcsc_part, "C6568") # CP2102-GMR - self.assign(self.actual_basic_part, True) + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground()) + self.regin = self.Port( + VoltageSink( + voltage_limits=(4.0, 5.25) * Volt, # Table 6 + current_draw=(0.080, 26) * mAmp, # TAble 3, suspended typ to normal max + ) + ) + self.vdd = self.Port( + VoltageSource( # as input, limits are 3.0-3.6v + voltage_out=(3.0, 3.6) * Volt, # Table 6 + current_limits=(0, 100) * mAmp, # Table 6 note + ) + ) + self.vbus = self.Port( + VoltageSink( + voltage_limits=(2.9, 5.8) * Volt, # Table 6 max VBUS threshold Table 2 maximum + # no current draw, is a sense input pin + ) + ) + + self.usb = self.Port(UsbDevicePort()) + + dio_model = DigitalBidir.from_supply( + self.gnd, + self.vdd, + current_limits=(-100, 100) * mAmp, # Table 2, assumed sunk is symmetric since no source rating is given + voltage_limit_abs=(-0.3, 5.8) * Volt, + input_threshold_abs=(0.8, 2.0) * Volt, # Table 4 + ) + din_model = DigitalSink.from_bidir(dio_model) + dout_model = DigitalSource.from_bidir(dio_model) + + self.rst = self.Port(din_model, optional=True) + self.suspend = self.Port(dout_model, optional=True) + self.nsuspend = self.Port(dout_model, optional=True) + + self.uart = self.Port(UartPort(dio_model), optional=True) # baud up to 921600bps + self.ri = self.Port(din_model, optional=True) + self.dcd = self.Port(din_model, optional=True) + self.dtr = self.Port(dout_model, optional=True) + self.dsr = self.Port(din_model, optional=True) + self.rts = self.Port(dout_model, optional=True) + self.cts = self.Port(din_model, optional=True) + + self.require( + self.uart.is_connected() + | self.ri.is_connected() + | self.dcd.is_connected() + | self.dtr.is_connected() + | self.dsr.is_connected() + | self.rts.is_connected() + | self.cts.is_connected() + ) + + self.footprint( + "U", + "Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.35x3.35mm", + { + "1": self.dcd, + "2": self.ri, + "3": self.gnd, + "4": self.usb.dp, + "5": self.usb.dm, + "6": self.vdd, + "7": self.regin, + "8": self.vbus, + "9": self.rst, + # 10: NC - per Table 9 can be unconnected or tied to Vdd + "11": self.nsuspend, + "12": self.suspend, + # 13-22: NC (18 on CP2109 is VPP) + "23": self.cts, + "24": self.rts, + "25": self.uart.rx, + "26": self.uart.tx, + "27": self.dsr, + "28": self.dtr, + "29": self.gnd, + }, + mfr="Silicon Labs", + part="CP2102", + datasheet="https://www.silabs.com/documents/public/data-sheets/CP2102-9.pdf", + ) + self.assign(self.lcsc_part, "C6568") # CP2102-GMR + self.assign(self.actual_basic_part, True) class Cp2102(Interface, PinMappable): - """USB-UART converter""" - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Cp2102_Device()) - self.pwr = self.Export(self.ic.regin, [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - - self.usb = self.Export(self.ic.usb) - self.suspend = self.Export(self.ic.suspend, optional=True) - self.nsuspend = self.Export(self.ic.nsuspend, optional=True) - - self.uart = self.Export(self.ic.uart, optional=True) - self.ri = self.Export(self.ic.ri, optional=True) - self.dcd = self.Export(self.ic.dcd, optional=True) - self.dtr = self.Export(self.ic.dtr, optional=True) - self.dsr = self.Export(self.ic.dsr, optional=True) - self.rts = self.Export(self.ic.rts, optional=True) - self.cts = self.Export(self.ic.cts, optional=True) - - @override - def contents(self) -> None: - super().contents() - self.connect(self.ic.regin, self.ic.vbus) - self.connect(self.ic.vdd.as_digital_source(), self.ic.rst) - - self.regin_cap0 = self.Block(DecouplingCapacitor(1.0*uFarad(tol=0.2))).connected(self.gnd, self.ic.regin) - self.regin_cap1 = self.Block(DecouplingCapacitor(0.1*uFarad(tol=0.2))).connected(self.gnd, self.ic.regin) - self.vdd_cap = self.Block(DecouplingCapacitor(1.0*uFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) - # Vdd currently isn't externally exposed, but if externally used a 4.7uF capacitor is needed (Figure 5 Option 2) + """USB-UART converter""" + + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Cp2102_Device()) + self.pwr = self.Export(self.ic.regin, [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + + self.usb = self.Export(self.ic.usb) + self.suspend = self.Export(self.ic.suspend, optional=True) + self.nsuspend = self.Export(self.ic.nsuspend, optional=True) + + self.uart = self.Export(self.ic.uart, optional=True) + self.ri = self.Export(self.ic.ri, optional=True) + self.dcd = self.Export(self.ic.dcd, optional=True) + self.dtr = self.Export(self.ic.dtr, optional=True) + self.dsr = self.Export(self.ic.dsr, optional=True) + self.rts = self.Export(self.ic.rts, optional=True) + self.cts = self.Export(self.ic.cts, optional=True) + + @override + def contents(self) -> None: + super().contents() + self.connect(self.ic.regin, self.ic.vbus) + self.connect(self.ic.vdd.as_digital_source(), self.ic.rst) + + self.regin_cap0 = self.Block(DecouplingCapacitor(1.0 * uFarad(tol=0.2))).connected(self.gnd, self.ic.regin) + self.regin_cap1 = self.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.regin) + self.vdd_cap = self.Block(DecouplingCapacitor(1.0 * uFarad(tol=0.2))).connected(self.gnd, self.ic.vdd) + # Vdd currently isn't externally exposed, but if externally used a 4.7uF capacitor is needed (Figure 5 Option 2) diff --git a/edg/parts/VoltageReferences.py b/edg/parts/VoltageReferences.py index 325a0602d..134952ec9 100644 --- a/edg/parts/VoltageReferences.py +++ b/edg/parts/VoltageReferences.py @@ -5,57 +5,62 @@ class Ref30xx_Device(InternalSubcircuit, LinearRegulatorDevice, GeneratorBlock, JlcPart, FootprintBlock): - def __init__(self, output_voltage: RangeLike): - super().__init__() - - self.assign(self.pwr_in.voltage_limits, (0, 5.5) * Volt) # table 7.3 - self.assign(self.pwr_out.current_limits, (0, 25) * mAmp) # table 7.3, load current - self.assign(self.actual_quiescent_current, (42, 59) * uAmp) # table 7.5 - self.assign(self.actual_dropout, (1, 50) * mVolt) # table 7.5 - - self.output_voltage = self.ArgParameter(output_voltage) - self.generator_param(self.output_voltage) - - @override - def generate(self) -> None: - super().generate() - parts = [ # output voltage, table 7.5 - (Range(1.2475, 1.2525), 'REF3012', 'C34674'), # REF3012AIDBZR - (Range(2.044, 2.052), 'REF3020', 'C26804'), # REF3020AIDBZR - (Range(2.495, 2.505), 'REF3025', 'C11334'), # REF3025AIDBZR - (Range(2.994, 3.006), 'REF3030', 'C38423'), # REF3030AIDBZR - (Range(3.294, 3.306), 'REF3033', 'C36658'), # REF3033AIDBZR - (Range(4.088, 4.104), 'REF3040', 'C19415'), # REF3040AIDBZR - ] - suitable_parts = [(part_out, part_number, lcsc_part) for part_out, part_number, lcsc_part in parts - if part_out.fuzzy_in(self.get(self.output_voltage))] - assert suitable_parts, "no regulator with compatible output" - part_output_voltage, part_number, lcsc_part = suitable_parts[0] - - self.assign(self.pwr_out.voltage_out, part_output_voltage) - self.assign(self.lcsc_part, lcsc_part) - self.assign(self.actual_basic_part, False) - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', - { - '1': self.pwr_in, - '2': self.pwr_out, - '3': self.gnd, - }, - mfr='Texas Instruments', part=part_number, - datasheet='https://www.ti.com/lit/ds/symlink/ref3030.pdf', - ) + def __init__(self, output_voltage: RangeLike): + super().__init__() + + self.assign(self.pwr_in.voltage_limits, (0, 5.5) * Volt) # table 7.3 + self.assign(self.pwr_out.current_limits, (0, 25) * mAmp) # table 7.3, load current + self.assign(self.actual_quiescent_current, (42, 59) * uAmp) # table 7.5 + self.assign(self.actual_dropout, (1, 50) * mVolt) # table 7.5 + + self.output_voltage = self.ArgParameter(output_voltage) + self.generator_param(self.output_voltage) + + @override + def generate(self) -> None: + super().generate() + parts = [ # output voltage, table 7.5 + (Range(1.2475, 1.2525), "REF3012", "C34674"), # REF3012AIDBZR + (Range(2.044, 2.052), "REF3020", "C26804"), # REF3020AIDBZR + (Range(2.495, 2.505), "REF3025", "C11334"), # REF3025AIDBZR + (Range(2.994, 3.006), "REF3030", "C38423"), # REF3030AIDBZR + (Range(3.294, 3.306), "REF3033", "C36658"), # REF3033AIDBZR + (Range(4.088, 4.104), "REF3040", "C19415"), # REF3040AIDBZR + ] + suitable_parts = [ + (part_out, part_number, lcsc_part) + for part_out, part_number, lcsc_part in parts + if part_out.fuzzy_in(self.get(self.output_voltage)) + ] + assert suitable_parts, "no regulator with compatible output" + part_output_voltage, part_number, lcsc_part = suitable_parts[0] + + self.assign(self.pwr_out.voltage_out, part_output_voltage) + self.assign(self.lcsc_part, lcsc_part) + self.assign(self.actual_basic_part, False) + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23", + { + "1": self.pwr_in, + "2": self.pwr_out, + "3": self.gnd, + }, + mfr="Texas Instruments", + part=part_number, + datasheet="https://www.ti.com/lit/ds/symlink/ref3030.pdf", + ) class Ref30xx(VoltageReference): - @override - def contents(self) -> None: - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.ic = imp.Block(Ref30xx_Device(self.output_voltage)) - # Section 9.1: 0.47uF cap recommended; no output cap required - self.in_cap = imp.Block(DecouplingCapacitor(capacitance=0.47 * uFarad(tol=0.2))) - - self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) - self.connect(self.pwr_out, self.ic.pwr_out) + @override + def contents(self) -> None: + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.ic = imp.Block(Ref30xx_Device(self.output_voltage)) + # Section 9.1: 0.47uF cap recommended; no output cap required + self.in_cap = imp.Block(DecouplingCapacitor(capacitance=0.47 * uFarad(tol=0.2))) + + self.connect(self.pwr_in, self.ic.pwr_in, self.in_cap.pwr) + self.connect(self.pwr_out, self.ic.pwr_out) diff --git a/edg/parts/__init__.py b/edg/parts/__init__.py index 3737ed294..8d45e14ed 100644 --- a/edg/parts/__init__.py +++ b/edg/parts/__init__.py @@ -43,10 +43,32 @@ from .PassiveConnector_Header import PinHeader254, PinHeader254Vertical, PinHeader254Horizontal, PinSocket254 from .PassiveConnector_Header import PinHeader254DualShroudedInline from .PassiveConnector_Header import PinHeader127DualShrouded -from .PassiveConnector_Header import JstXh, JstXhAHorizontal, JstXhAVertical, JstPh, JstPhKVertical, JstPhSmVertical,\ - JstPhKHorizontal, JstPhSmVerticalJlc, JstShSmHorizontal, MolexSl, Picoblade, Picoblade53398, Picoblade53261 +from .PassiveConnector_Header import ( + JstXh, + JstXhAHorizontal, + JstXhAVertical, + JstPh, + JstPhKVertical, + JstPhSmVertical, + JstPhKHorizontal, + JstPhSmVerticalJlc, + JstShSmHorizontal, + MolexSl, + Picoblade, + Picoblade53398, + Picoblade53261, +) from .PassiveConnector_Fpc import Fpc030, Fpc030Top, Fpc030Bottom, Fpc030TopBottom, HiroseFh35cshw -from .PassiveConnector_Fpc import Fpc050, Fpc050Top, Fpc050Bottom, Fpc050BottomFlip, HiroseFh12sh, Afc01, Afc07Top, Te1734839 +from .PassiveConnector_Fpc import ( + Fpc050, + Fpc050Top, + Fpc050Bottom, + Fpc050BottomFlip, + HiroseFh12sh, + Afc01, + Afc07Top, + Te1734839, +) from .PassiveConnector_TagConnect import TagConnect, TagConnectLegged, TagConnectNonLegged from .Jumpers import SolderJumperTriangular @@ -67,7 +89,17 @@ from .SwitchedCap_TexasInstruments import Lm2664 from .BuckConverter_Custom import CustomSyncBuckConverterIndependent from .BuckBoostConverter_Custom import CustomSyncBuckBoostConverterPwm -from .PowerConditioning import BufferedSupply, Supercap, SingleDiodePowerMerge, DiodePowerMerge, PriorityPowerOr, SoftPowerGate, SoftPowerSwitch, PmosReverseProtection, PmosChargerReverseProtection +from .PowerConditioning import ( + BufferedSupply, + Supercap, + SingleDiodePowerMerge, + DiodePowerMerge, + PriorityPowerOr, + SoftPowerGate, + SoftPowerSwitch, + PmosReverseProtection, + PmosChargerReverseProtection, +) from .LedDriver_Al8861 import Al8861 from .LedDriver_Tps92200 import Tps92200 from .ResetGenerator_Apx803s import Apx803s @@ -80,7 +112,12 @@ from .Microcontroller_Stm32g431 import Stm32g431kb from .Microcontroller_Stm32l432 import Stm32l432k from .Microcontroller_nRF52840 import Holyiot_18010, Mdbt50q_1mv2, Feather_Nrf52840 -from .Microcontroller_Esp import EspProgrammingHeader, EspProgrammingAutoReset, EspProgrammingPinHeader254, EspProgrammingTc2030 +from .Microcontroller_Esp import ( + EspProgrammingHeader, + EspProgrammingAutoReset, + EspProgrammingPinHeader254, + EspProgrammingTc2030, +) from .Microcontroller_Esp import HasEspProgramming from .Microcontroller_Esp import EspAutoProgram from .Microcontroller_Esp32 import Esp32_Wroom_32, Freenove_Esp32_Wrover @@ -113,8 +150,16 @@ from .Rtc_Pcf2129 import Pcf2129 from .RfModules import Xbee_S3b, BlueSmirf -from .Neopixel import Neopixel, Ws2812b, Sk6812Mini_E, Sk6805_Ec15, Ws2812c_2020, Sk6812_Side_A, \ - NeopixelArray, NeopixelArrayCircular +from .Neopixel import ( + Neopixel, + Ws2812b, + Sk6812Mini_E, + Sk6805_Ec15, + Ws2812c_2020, + Sk6812_Side_A, + NeopixelArray, + NeopixelArrayCircular, +) from .Lcd_Qt096t_if09 import Qt096t_if09 from .Lcd_Ch280qv10_Ct import Ch280qv10_Ct from .Lcd_Er_Tft1_28_3 import Er_Tft_128_3 @@ -189,14 +234,17 @@ # compatibility shims import deprecated as __deprecated # not to be exported + @__deprecated.deprecated("new naming convention") class Vl53l0xApplication(Vl53l0x, DeprecatedBlock): pass + @__deprecated.deprecated("new naming convention") class Imu_Lsm6ds3trc(Lsm6ds3trc, DeprecatedBlock): pass + @__deprecated.deprecated("new naming convention") class Mag_Qmc5883l(Qmc5883l, DeprecatedBlock): pass diff --git a/edg/parts/resources/jlcprune.py b/edg/parts/resources/jlcprune.py index 8e07dbcdf..669649bfe 100644 --- a/edg/parts/resources/jlcprune.py +++ b/edg/parts/resources/jlcprune.py @@ -1,66 +1,59 @@ KEEP_CATEGORIES = [ - 'Chip Resistor - Surface Mount', - 'Current Sense Resistors/Shunt Resistors', - 'Low Resistors & Current Sense Resistors - Surface Mount', - 'Resistor Networks & Arrays', - 'Through Hole Resistors', - - - 'Aluminum Electrolytic Capacitors - Leaded', - 'Aluminum Electrolytic Capacitors - SMD', - 'Multilayer Ceramic Capacitors MLCC - Leaded', - 'Multilayer Ceramic Capacitors MLCC - SMD/SMT', - 'Tantalum Capacitors', - - 'Fuse Holders', - 'Fuses', - 'Resettable Fuses', - - 'Crystals', - 'Pre-programmed Oscillators', - 'Oscillators', - - 'Diodes - Fast Recovery Rectifiers', - 'Diodes - General Purpose', - 'Schottky Barrier Diodes (SBD)', - 'Switching Diode', - - 'Zener Diodes', - - 'Inductors (SMD)', - 'Power Inductors', - 'Through Hole Inductors', - 'Ferrite Beads', - - 'Infrared (IR) LEDs', - 'Light Emitting Diodes (LED)', - 'Ultra Violet LEDs', - - 'Bipolar Transistors - BJT', - 'MOSFETs', - - "Antennas", + "Chip Resistor - Surface Mount", + "Current Sense Resistors/Shunt Resistors", + "Low Resistors & Current Sense Resistors - Surface Mount", + "Resistor Networks & Arrays", + "Through Hole Resistors", + "Aluminum Electrolytic Capacitors - Leaded", + "Aluminum Electrolytic Capacitors - SMD", + "Multilayer Ceramic Capacitors MLCC - Leaded", + "Multilayer Ceramic Capacitors MLCC - SMD/SMT", + "Tantalum Capacitors", + "Fuse Holders", + "Fuses", + "Resettable Fuses", + "Crystals", + "Pre-programmed Oscillators", + "Oscillators", + "Diodes - Fast Recovery Rectifiers", + "Diodes - General Purpose", + "Schottky Barrier Diodes (SBD)", + "Switching Diode", + "Zener Diodes", + "Inductors (SMD)", + "Power Inductors", + "Through Hole Inductors", + "Ferrite Beads", + "Infrared (IR) LEDs", + "Light Emitting Diodes (LED)", + "Ultra Violet LEDs", + "Bipolar Transistors - BJT", + "MOSFETs", + "Antennas", ] MIN_STOCK = 100 # arbitrary cutoff to prune rare parts -FILE = 'JLCPCB SMT Parts Library(20220419).csv' - -if __name__ == '__main__': - # Not using a standard CSV parser to preserve the original formatting as much as possible - with open(f'{FILE}', 'rb') as full_file, open(f'Pruned_{FILE}', 'wb') as out_file: - header = full_file.readline() - out_file.writelines([header]) - rows = str(header.decode('gb2312').strip()).split(',') - CATEGORY_INDEX = rows.index('Second Category') - STOCK_INDEX = -2 # hard-coded because str.split doesn't account for quoted elements - - wrote_lines = 0 - for i, line in enumerate(full_file.readlines()): - values = str(line.decode('gb2312').strip()).split(',') - if values[CATEGORY_INDEX] in KEEP_CATEGORIES and \ - values[STOCK_INDEX] and int(values[STOCK_INDEX]) >= MIN_STOCK: - out_file.writelines([line]) - wrote_lines += 1 - - print(f"read {i} lines, wrote {wrote_lines} lines") +FILE = "JLCPCB SMT Parts Library(20220419).csv" + +if __name__ == "__main__": + # Not using a standard CSV parser to preserve the original formatting as much as possible + with open(f"{FILE}", "rb") as full_file, open(f"Pruned_{FILE}", "wb") as out_file: + header = full_file.readline() + out_file.writelines([header]) + rows = str(header.decode("gb2312").strip()).split(",") + CATEGORY_INDEX = rows.index("Second Category") + STOCK_INDEX = -2 # hard-coded because str.split doesn't account for quoted elements + + wrote_lines = 0 + for i, line in enumerate(full_file.readlines()): + values = str(line.decode("gb2312").strip()).split(",") + if ( + values[CATEGORY_INDEX] in KEEP_CATEGORIES + and values[STOCK_INDEX] + and int(values[STOCK_INDEX]) >= MIN_STOCK + ): + out_file.writelines([line]) + wrote_lines += 1 + + print(f"read {i} lines, wrote {wrote_lines} lines") diff --git a/edg/parts/test_JlcCapacitor.py b/edg/parts/test_JlcCapacitor.py index 380878b7c..ff6071e14 100644 --- a/edg/parts/test_JlcCapacitor.py +++ b/edg/parts/test_JlcCapacitor.py @@ -3,60 +3,55 @@ class JlcCapacitorTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(JlcCapacitor( - capacitance=10 * nFarad(tol=0.1), - voltage=(0, 3.3) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(JlcCapacitor(capacitance=10 * nFarad(tol=0.1), voltage=(0, 3.3) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) class JlcBigCapacitorTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(JlcCapacitor( - capacitance=(50, 1000) * uFarad, - voltage=(0, 3.3) * Volt - )) - (self.dummya, ), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(JlcCapacitor(capacitance=(50, 1000) * uFarad, voltage=(0, 3.3) * Volt)) + (self.dummya,), _ = self.chain(self.dut.pos, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.neg, self.Block(DummyPassive())) class CapacitorTestCase(unittest.TestCase): - def test_capacitor(self) -> None: - compiled = ScalaCompiler.compile(JlcCapacitorTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_0402_1005Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_part']), 'CL05B103KB5NNNC') - self.assertEqual(compiled.get_value(['dut', 'lcsc_part']), 'C15195') - - def test_capacitor_part(self) -> None: - compiled = ScalaCompiler.compile(JlcCapacitorTestTop, Refinements( - instance_values=[(['dut', 'part'], 'CL21B103KBANNNC')] - )) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_0805_2012Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_part']), 'CL21B103KBANNNC') - - def test_capacitor_min_package(self) -> None: - compiled = ScalaCompiler.compile(JlcCapacitorTestTop, Refinements( - instance_values=[(['dut', 'footprint_area'], Range.from_lower(4.0))] - )) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_0603_1608Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_part']), '0603B103K500NT') - self.assertEqual(compiled.get_value(['dut', 'lcsc_part']), 'C57112') - - def test_capacitor_footprint(self) -> None: - compiled = ScalaCompiler.compile(JlcCapacitorTestTop, Refinements( - instance_values=[(['dut', 'footprint_spec'], 'Capacitor_SMD:C_0805_2012Metric')] - )) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Capacitor_SMD:C_0805_2012Metric') - - def test_multi_capacitor(self) -> None: - """The example returns multiple capacitors due to accounting for ±10% tolerance. - Minimal test to avoid flakiness from part table updates.""" - compiled = ScalaCompiler.compile(JlcBigCapacitorTestTop) - self.assertNotEqual(compiled.get_value(['dut', 'c[0]', 'fp_footprint']), None) - self.assertNotEqual(compiled.get_value(['dut', 'c[1]', 'fp_footprint']), None) - # should not be more than 6 - self.assertEqual(compiled.get_value(['dut', 'c[6]', 'fp_footprint']), None) + def test_capacitor(self) -> None: + compiled = ScalaCompiler.compile(JlcCapacitorTestTop) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_0402_1005Metric") + self.assertEqual(compiled.get_value(["dut", "fp_part"]), "CL05B103KB5NNNC") + self.assertEqual(compiled.get_value(["dut", "lcsc_part"]), "C15195") + + def test_capacitor_part(self) -> None: + compiled = ScalaCompiler.compile( + JlcCapacitorTestTop, Refinements(instance_values=[(["dut", "part"], "CL21B103KBANNNC")]) + ) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_0805_2012Metric") + self.assertEqual(compiled.get_value(["dut", "fp_part"]), "CL21B103KBANNNC") + + def test_capacitor_min_package(self) -> None: + compiled = ScalaCompiler.compile( + JlcCapacitorTestTop, Refinements(instance_values=[(["dut", "footprint_area"], Range.from_lower(4.0))]) + ) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_0603_1608Metric") + self.assertEqual(compiled.get_value(["dut", "fp_part"]), "0603B103K500NT") + self.assertEqual(compiled.get_value(["dut", "lcsc_part"]), "C57112") + + def test_capacitor_footprint(self) -> None: + compiled = ScalaCompiler.compile( + JlcCapacitorTestTop, + Refinements(instance_values=[(["dut", "footprint_spec"], "Capacitor_SMD:C_0805_2012Metric")]), + ) + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Capacitor_SMD:C_0805_2012Metric") + + def test_multi_capacitor(self) -> None: + """The example returns multiple capacitors due to accounting for ±10% tolerance. + Minimal test to avoid flakiness from part table updates.""" + compiled = ScalaCompiler.compile(JlcBigCapacitorTestTop) + self.assertNotEqual(compiled.get_value(["dut", "c[0]", "fp_footprint"]), None) + self.assertNotEqual(compiled.get_value(["dut", "c[1]", "fp_footprint"]), None) + # should not be more than 6 + self.assertEqual(compiled.get_value(["dut", "c[6]", "fp_footprint"]), None) diff --git a/edg/parts/test_JlcResistor.py b/edg/parts/test_JlcResistor.py index 44fddcb9e..2b8a50ba2 100644 --- a/edg/parts/test_JlcResistor.py +++ b/edg/parts/test_JlcResistor.py @@ -3,20 +3,17 @@ class JlcResistorTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(JlcResistor( - resistance=750 * Ohm(tol=0.10), - power=(0, 0.25) * Watt - )) - (self.dummya, ), _ = self.chain(self.dut.a, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.b, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(JlcResistor(resistance=750 * Ohm(tol=0.10), power=(0, 0.25) * Watt)) + (self.dummya,), _ = self.chain(self.dut.a, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.b, self.Block(DummyPassive())) class JlcResistorTestCase(unittest.TestCase): - def test_resistor(self) -> None: - compiled = ScalaCompiler.compile(JlcResistorTestTop) + def test_resistor(self) -> None: + compiled = ScalaCompiler.compile(JlcResistorTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Resistor_SMD:R_1206_3216Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_part']), '1206W4F7500T5E') - self.assertEqual(compiled.get_value(['dut', 'lcsc_part']), 'C17985') + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Resistor_SMD:R_1206_3216Metric") + self.assertEqual(compiled.get_value(["dut", "fp_part"]), "1206W4F7500T5E") + self.assertEqual(compiled.get_value(["dut", "lcsc_part"]), "C17985") diff --git a/edg/parts/test_inductor.py b/edg/parts/test_inductor.py index 8fc3521a2..a3d3ca31f 100644 --- a/edg/parts/test_inductor.py +++ b/edg/parts/test_inductor.py @@ -4,20 +4,22 @@ class JlcInductorTestTop(Block): - def __init__(self) -> None: - super().__init__() - self.dut = self.Block(JlcInductor( - inductance=2.2 * uHenry(tol=0.2), - current=(0, 500) * mAmp, - # no frequency spec since JLC doesn't allow it - )) - (self.dummya, ), _ = self.chain(self.dut.a, self.Block(DummyPassive())) - (self.dummyb, ), _ = self.chain(self.dut.b, self.Block(DummyPassive())) + def __init__(self) -> None: + super().__init__() + self.dut = self.Block( + JlcInductor( + inductance=2.2 * uHenry(tol=0.2), + current=(0, 500) * mAmp, + # no frequency spec since JLC doesn't allow it + ) + ) + (self.dummya,), _ = self.chain(self.dut.a, self.Block(DummyPassive())) + (self.dummyb,), _ = self.chain(self.dut.b, self.Block(DummyPassive())) class InductorTestCase(unittest.TestCase): - def test_inductor(self) -> None: - compiled = ScalaCompiler.compile(JlcInductorTestTop) + def test_inductor(self) -> None: + compiled = ScalaCompiler.compile(JlcInductorTestTop) - self.assertEqual(compiled.get_value(['dut', 'fp_footprint']), 'Inductor_SMD:L_0603_1608Metric') - self.assertEqual(compiled.get_value(['dut', 'fp_part']), 'CMH160808B2R2MT') + self.assertEqual(compiled.get_value(["dut", "fp_footprint"]), "Inductor_SMD:L_0603_1608Metric") + self.assertEqual(compiled.get_value(["dut", "fp_part"]), "CMH160808B2R2MT") diff --git a/edg/parts/test_kicad_import_jlc.py b/edg/parts/test_kicad_import_jlc.py index 64986d047..1f4261386 100644 --- a/edg/parts/test_kicad_import_jlc.py +++ b/edg/parts/test_kicad_import_jlc.py @@ -8,6 +8,7 @@ class JlcBlackboxBlock(KiCadSchematicBlock): """Block with a blackbox part, a sub-blocks that only knows it has a footprint and pins and doesn't map to one of the abstract types.""" + def __init__(self) -> None: super().__init__() self.pwr = self.Port(Passive.empty()) @@ -23,47 +24,50 @@ def test_import_blackbox(self) -> None: constraints = list(map(lambda pair: pair.value, pb.constraints)) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'pwr' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'U1' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'ports' - expected_conn.exported.internal_block_port.ref.steps.add().allocate = '1' + expected_conn.exported.exterior_port.ref.steps.add().name = "pwr" + expected_conn.exported.internal_block_port.ref.steps.add().name = "U1" + expected_conn.exported.internal_block_port.ref.steps.add().name = "ports" + expected_conn.exported.internal_block_port.ref.steps.add().allocate = "1" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'gnd' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'U1' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'ports' - expected_conn.exported.internal_block_port.ref.steps.add().allocate = '3' + expected_conn.exported.exterior_port.ref.steps.add().name = "gnd" + expected_conn.exported.internal_block_port.ref.steps.add().name = "U1" + expected_conn.exported.internal_block_port.ref.steps.add().name = "ports" + expected_conn.exported.internal_block_port.ref.steps.add().allocate = "3" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'U1' - expected_conn.connected.block_port.ref.steps.add().name = 'ports' - expected_conn.connected.block_port.ref.steps.add().allocate = '2' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "U1" + expected_conn.connected.block_port.ref.steps.add().name = "ports" + expected_conn.connected.block_port.ref.steps.add().allocate = "2" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.connected.link_port.ref.steps.add().name = 'node' - expected_conn.connected.link_port.ref.steps.add().name = 'passives' - expected_conn.connected.link_port.ref.steps.add().allocate = '' - expected_conn.connected.block_port.ref.steps.add().name = 'res' - expected_conn.connected.block_port.ref.steps.add().name = 'a' + expected_conn.connected.link_port.ref.steps.add().name = "node" + expected_conn.connected.link_port.ref.steps.add().name = "passives" + expected_conn.connected.link_port.ref.steps.add().allocate = "" + expected_conn.connected.block_port.ref.steps.add().name = "res" + expected_conn.connected.block_port.ref.steps.add().name = "a" self.assertIn(expected_conn, constraints) expected_conn = edgir.ValueExpr() - expected_conn.exported.exterior_port.ref.steps.add().name = 'out' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'res' - expected_conn.exported.internal_block_port.ref.steps.add().name = 'b' + expected_conn.exported.exterior_port.ref.steps.add().name = "out" + expected_conn.exported.internal_block_port.ref.steps.add().name = "res" + expected_conn.exported.internal_block_port.ref.steps.add().name = "b" self.assertIn(expected_conn, constraints) # resistor not checked, responsibility of another test # U1.kicad_pins not checked, because array assign syntax is wonky - self.assertIn(edgir.AssignLit(['U1', 'kicad_refdes_prefix'], 'U'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_footprint'], 'Package_TO_SOT_SMD:SOT-23'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_part'], 'Sensor_Temperature:MCP9700AT-ETT'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_value'], 'MCP9700AT-ETT'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_datasheet'], 'http://ww1.microchip.com/downloads/en/DeviceDoc/21942e.pdf'), constraints) - self.assertIn(edgir.AssignLit(['U1', 'kicad_jlcpcb_part'], 'C127949'), constraints) + self.assertIn(edgir.AssignLit(["U1", "kicad_refdes_prefix"], "U"), constraints) + self.assertIn(edgir.AssignLit(["U1", "kicad_footprint"], "Package_TO_SOT_SMD:SOT-23"), constraints) + self.assertIn(edgir.AssignLit(["U1", "kicad_part"], "Sensor_Temperature:MCP9700AT-ETT"), constraints) + self.assertIn(edgir.AssignLit(["U1", "kicad_value"], "MCP9700AT-ETT"), constraints) + self.assertIn( + edgir.AssignLit(["U1", "kicad_datasheet"], "http://ww1.microchip.com/downloads/en/DeviceDoc/21942e.pdf"), + constraints, + ) + self.assertIn(edgir.AssignLit(["U1", "kicad_jlcpcb_part"], "C127949"), constraints) diff --git a/edg/parts/test_nfc.py b/edg/parts/test_nfc.py index 88a9a6c91..b10d5bf37 100644 --- a/edg/parts/test_nfc.py +++ b/edg/parts/test_nfc.py @@ -6,29 +6,34 @@ class NfcAntennaTest(unittest.TestCase): def test_antenna(self) -> None: # from https://www.eetimes.eu/impedance-matching-for-nfc-applications-part-2/ - self.assertAlmostEqual(NfcAntenna.impedance_from_lrc(13.56e6, 0.7e-6, 1.7, 9.12e-12), - complex(1.87, 62.53), delta=0.01) + self.assertAlmostEqual( + NfcAntenna.impedance_from_lrc(13.56e6, 0.7e-6, 1.7, 9.12e-12), complex(1.87, 62.53), delta=0.01 + ) # measured example from https://www.nxp.com/docs/en/application-note/AN13219.pdf # stray capacitance seems to have been lumped into the inductance - self.assertAlmostEqual(NfcAntenna.impedance_from_lrc(13.56e6, 1.523773e-6, 1.372, 0e-12), - complex(1.372, 129.825), delta=0.001) + self.assertAlmostEqual( + NfcAntenna.impedance_from_lrc(13.56e6, 1.523773e-6, 1.372, 0e-12), complex(1.372, 129.825), delta=0.001 + ) # from https://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/667/Impedance-matching-for-13.56-MHz-NFC-antennas-without-VNA.pdf - self.assertAlmostEqual(NfcAntenna.impedance_from_lrc(13.56e6, 1.67e-6, 1.66, 0e-12), - complex(1.66, 142.28), delta=0.01) - self.assertAlmostEqual(NfcAntenna.impedance_from_lrc(13.56e6, 1.03e-6, 0.51, 0e-12), - complex(0.51, 87.76), delta=0.01) + self.assertAlmostEqual( + NfcAntenna.impedance_from_lrc(13.56e6, 1.67e-6, 1.66, 0e-12), complex(1.66, 142.28), delta=0.01 + ) + self.assertAlmostEqual( + NfcAntenna.impedance_from_lrc(13.56e6, 1.03e-6, 0.51, 0e-12), complex(0.51, 87.76), delta=0.01 + ) def test_damp_res(self) -> None: # excel matching from https://www.nxp.com/docs/en/application-note/AN13219.pdf ant_z = NfcAntenna.impedance_from_lrc(13.56e6, 1522e-9, 1.40, 0.1e-12) - self.assertAlmostEqual(NfcAntennaDampening.damp_res_from_impedance(ant_z, 20), 2.54*2, delta=0.01) + self.assertAlmostEqual(NfcAntennaDampening.damp_res_from_impedance(ant_z, 20), 2.54 * 2, delta=0.01) def test_lc(self) -> None: # asymmetrical matching case from https://www.nxp.com/docs/en/application-note/AN13219.pdf - self.assertAlmostEqual(DifferentialLcLowpassFilter._calculate_capacitance(22e6, 160e-9), - 327.1e-12, delta=0.1e-12) + self.assertAlmostEqual( + DifferentialLcLowpassFilter._calculate_capacitance(22e6, 160e-9), 327.1e-12, delta=0.1e-12 + ) def test_l_match(self) -> None: # from https://www.eetimes.eu/impedance-matching-for-nfc-applications-part-2/ @@ -38,26 +43,34 @@ def test_l_match(self) -> None: self.assertAlmostEqual(source_z, complex(165.82, -45.46), delta=0.01) ant_z = NfcAntenna.impedance_from_lrc(13.56e6, 0.7e-6, 1.7, 9.12e-12) self.assertAlmostEqual(ant_z, complex(1.87, 62.53), delta=0.01) - diff_cs, diff_cp = DifferentialLLowPassFilter._calculate_se_values(13.56e6, source_z.conjugate(), ant_z.conjugate()) + diff_cs, diff_cp = DifferentialLLowPassFilter._calculate_se_values( + 13.56e6, source_z.conjugate(), ant_z.conjugate() + ) self.assertAlmostEqual(-diff_cs * 2, 45e-12, delta=1e-12) # TODO why is this negative self.assertAlmostEqual(-diff_cp * 2, 337e-12, delta=1e-12) # from https://www.mroland.at/publications/2008-roland-csndsp/Roland_2008_CSNDSP08_AutomaticImpedanceMatching.pdf source_z = NfcAntenna.impedance_from_lrc(13.56e6, 560e-9 * 2, 50, 220e-12 / 2) # common EMC filter ant_z = NfcAntenna.impedance_from_lrc(13.56e6, 3.12e-6, 4.93, 9.84e-12) - diff_cs, diff_cp = DifferentialLLowPassFilter._calculate_se_values(13.56e6, source_z.conjugate(), ant_z.conjugate()) + diff_cs, diff_cp = DifferentialLLowPassFilter._calculate_se_values( + 13.56e6, source_z.conjugate(), ant_z.conjugate() + ) self.assertAlmostEqual(-diff_cs * 2, 14e-12, delta=1e-12) self.assertAlmostEqual(-diff_cp * 2, 55e-12, delta=1e-12) ant_z = NfcAntenna.impedance_from_lrc(13.56e6, 1.7e-6, 4.83, 6.99e-12) - diff_cs, diff_cp = DifferentialLLowPassFilter._calculate_se_values(13.56e6, source_z.conjugate(), ant_z.conjugate()) + diff_cs, diff_cp = DifferentialLLowPassFilter._calculate_se_values( + 13.56e6, source_z.conjugate(), ant_z.conjugate() + ) self.assertAlmostEqual(-diff_cs * 2, 27e-12, delta=1e-12) self.assertAlmostEqual(-diff_cp * 2, 124e-12, delta=1e-12) # excel matching from https://www.nxp.com/docs/en/application-note/AN13219.pdf # source_z = NfcAntenna.impedance_from_lrc(13.56e6, 160e-9 * 2, 20, 330e-12) # doesn't work source_z = complex(44, 24) # this works, but unclear how this was obtained - ant_z = NfcAntenna.impedance_from_lrc(13.56e6, 1522e-9, 1.40, 0.1e-12) + 2.54*2 - diff_cs, diff_cp = DifferentialLLowPassFilter._calculate_se_values(13.56e6, source_z.conjugate(), ant_z.conjugate()) + ant_z = NfcAntenna.impedance_from_lrc(13.56e6, 1522e-9, 1.40, 0.1e-12) + 2.54 * 2 + diff_cs, diff_cp = DifferentialLLowPassFilter._calculate_se_values( + 13.56e6, source_z.conjugate(), ant_z.conjugate() + ) self.assertAlmostEqual(-diff_cs * 2, 65.1e-12, delta=0.2e-12) self.assertAlmostEqual(-diff_cp * 2, 111.0e-12, delta=0.7e-12) diff --git a/examples/IotFan/resources/rename_parts.py b/examples/IotFan/resources/rename_parts.py index 2d65e772a..39da486d4 100644 --- a/examples/IotFan/resources/rename_parts.py +++ b/examples/IotFan/resources/rename_parts.py @@ -15,35 +15,35 @@ # (first component matching the original name takes the first name in the rename list, # second takes the second name, and so on) remap_table: Dict[str, List[str]] = {} -remap_table['led'] = [] +remap_table["led"] = [] for led in range(24): - remap_table['led'].append(f'rgb_ring.led[{led}]') + remap_table["led"].append(f"rgb_ring.led[{led}]") -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Rename refdeses in a PCB.') - parser.add_argument('input_pcb', type=argparse.FileType('r')) - parser.add_argument('output_pcb', type=argparse.FileType('w')) - args = parser.parse_args() +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Rename refdeses in a PCB.") + parser.add_argument("input_pcb", type=argparse.FileType("r")) + parser.add_argument("output_pcb", type=argparse.FileType("w")) + args = parser.parse_args() - pcb_data = args.input_pcb.read() - pcb_sexp = sexpdata.loads(pcb_data) - assert parse_symbol(pcb_sexp[0]) == 'kicad_pcb' - pcb_dict = group_by_car(pcb_sexp) - for footprint in pcb_dict['footprint']: - footprint_dict = group_by_car(footprint) - for fp_text in footprint_dict['fp_text']: - if fp_text[1] == sexpdata.Symbol('reference'): - old_refdes = fp_text[2] - if old_refdes in remap_table: - remap_list = remap_table[old_refdes] - if remap_list: - fp_text[2] = remap_list.pop(0) - print(f"rename {old_refdes} => {fp_text[2]}") - else: - print(f"rename {old_refdes} failed, empty list") + pcb_data = args.input_pcb.read() + pcb_sexp = sexpdata.loads(pcb_data) + assert parse_symbol(pcb_sexp[0]) == "kicad_pcb" + pcb_dict = group_by_car(pcb_sexp) + for footprint in pcb_dict["footprint"]: + footprint_dict = group_by_car(footprint) + for fp_text in footprint_dict["fp_text"]: + if fp_text[1] == sexpdata.Symbol("reference"): + old_refdes = fp_text[2] + if old_refdes in remap_table: + remap_list = remap_table[old_refdes] + if remap_list: + fp_text[2] = remap_list.pop(0) + print(f"rename {old_refdes} => {fp_text[2]}") + else: + print(f"rename {old_refdes} failed, empty list") - modified_string = sexpdata.dumps(pcb_sexp) - # sexpdata doesn't insert newlines, and KiCad chokes when there's one long line - modified_string = modified_string.replace('))', '))\n') - args.output_pcb.write(modified_string) - print("Wrote PCB") + modified_string = sexpdata.dumps(pcb_sexp) + # sexpdata doesn't insert newlines, and KiCad chokes when there's one long line + modified_string = modified_string.replace("))", "))\n") + args.output_pcb.write(modified_string) + print("Wrote PCB") diff --git a/examples/IotKnob/resources/rename_parts.py b/examples/IotKnob/resources/rename_parts.py index bda1743ad..17fffd79f 100644 --- a/examples/IotKnob/resources/rename_parts.py +++ b/examples/IotKnob/resources/rename_parts.py @@ -15,40 +15,40 @@ # (first component matching the original name takes the first name in the rename list, # second takes the second name, and so on) remap_table: Dict[str, List[str]] = {} -remap_table['led'] = [] -remap_table['sw'] = [] +remap_table["led"] = [] +remap_table["sw"] = [] for led in range(24): - remap_table['led'].append(f'rgb_ring.led[{led}]') + remap_table["led"].append(f"rgb_ring.led[{led}]") for sw in range(6): - remap_table['sw'].append(f'sw[{sw}]') + remap_table["sw"].append(f"sw[{sw}]") for sw in range(12): - remap_table['led'].append(f'rgb_sw.led[{sw}]') + remap_table["led"].append(f"rgb_sw.led[{sw}]") -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Rename refdeses in a PCB.') - parser.add_argument('input_pcb', type=argparse.FileType('r')) - parser.add_argument('output_pcb', type=argparse.FileType('w')) - args = parser.parse_args() +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Rename refdeses in a PCB.") + parser.add_argument("input_pcb", type=argparse.FileType("r")) + parser.add_argument("output_pcb", type=argparse.FileType("w")) + args = parser.parse_args() - pcb_data = args.input_pcb.read() - pcb_sexp = sexpdata.loads(pcb_data) - assert parse_symbol(pcb_sexp[0]) == 'kicad_pcb' - pcb_dict = group_by_car(pcb_sexp) - for footprint in pcb_dict['footprint']: - footprint_dict = group_by_car(footprint) - for fp_text in footprint_dict['fp_text']: - if fp_text[1] == sexpdata.Symbol('reference'): - old_refdes = fp_text[2] - if old_refdes in remap_table: - remap_list = remap_table[old_refdes] - if remap_list: - fp_text[2] = remap_list.pop(0) - print(f"rename {old_refdes} => {fp_text[2]}") - else: - print(f"rename {old_refdes} failed, empty list") + pcb_data = args.input_pcb.read() + pcb_sexp = sexpdata.loads(pcb_data) + assert parse_symbol(pcb_sexp[0]) == "kicad_pcb" + pcb_dict = group_by_car(pcb_sexp) + for footprint in pcb_dict["footprint"]: + footprint_dict = group_by_car(footprint) + for fp_text in footprint_dict["fp_text"]: + if fp_text[1] == sexpdata.Symbol("reference"): + old_refdes = fp_text[2] + if old_refdes in remap_table: + remap_list = remap_table[old_refdes] + if remap_list: + fp_text[2] = remap_list.pop(0) + print(f"rename {old_refdes} => {fp_text[2]}") + else: + print(f"rename {old_refdes} failed, empty list") - modified_string = sexpdata.dumps(pcb_sexp) - # sexpdata doesn't insert newlines, and KiCad chokes when there's one long line - modified_string = modified_string.replace('))', '))\n') - args.output_pcb.write(modified_string) - print("Wrote PCB") + modified_string = sexpdata.dumps(pcb_sexp) + # sexpdata doesn't insert newlines, and KiCad chokes when there's one long line + modified_string = modified_string.replace("))", "))\n") + args.output_pcb.write(modified_string) + print("Wrote PCB") diff --git a/examples/RobotOwl/resources/rename_parts.py b/examples/RobotOwl/resources/rename_parts.py index c4c26b78a..89e9a14be 100644 --- a/examples/RobotOwl/resources/rename_parts.py +++ b/examples/RobotOwl/resources/rename_parts.py @@ -15,35 +15,35 @@ # (first component matching the original name takes the first name in the rename list, # second takes the second name, and so on) remap_table: Dict[str, List[str]] = {} -remap_table['led'] = [] +remap_table["led"] = [] for led in range(12): - remap_table['led'].append(f'ws2812bArray.led[{led}]') + remap_table["led"].append(f"ws2812bArray.led[{led}]") -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Rename refdeses in a PCB.') - parser.add_argument('input_pcb', type=argparse.FileType('r')) - parser.add_argument('output_pcb', type=argparse.FileType('w')) - args = parser.parse_args() +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Rename refdeses in a PCB.") + parser.add_argument("input_pcb", type=argparse.FileType("r")) + parser.add_argument("output_pcb", type=argparse.FileType("w")) + args = parser.parse_args() - pcb_data = args.input_pcb.read() - pcb_sexp = sexpdata.loads(pcb_data) - assert parse_symbol(pcb_sexp[0]) == 'kicad_pcb' - pcb_dict = group_by_car(pcb_sexp) - for footprint in pcb_dict['footprint']: - footprint_dict = group_by_car(footprint) - for fp_text in footprint_dict['fp_text']: - if fp_text[1] == sexpdata.Symbol('reference'): - old_refdes = fp_text[2] - if old_refdes in remap_table: - remap_list = remap_table[old_refdes] - if remap_list: - fp_text[2] = remap_list.pop(0) - print(f"rename {old_refdes} => {fp_text[2]}") - else: - print(f"rename {old_refdes} failed, empty list") + pcb_data = args.input_pcb.read() + pcb_sexp = sexpdata.loads(pcb_data) + assert parse_symbol(pcb_sexp[0]) == "kicad_pcb" + pcb_dict = group_by_car(pcb_sexp) + for footprint in pcb_dict["footprint"]: + footprint_dict = group_by_car(footprint) + for fp_text in footprint_dict["fp_text"]: + if fp_text[1] == sexpdata.Symbol("reference"): + old_refdes = fp_text[2] + if old_refdes in remap_table: + remap_list = remap_table[old_refdes] + if remap_list: + fp_text[2] = remap_list.pop(0) + print(f"rename {old_refdes} => {fp_text[2]}") + else: + print(f"rename {old_refdes} failed, empty list") - modified_string = sexpdata.dumps(pcb_sexp) - # sexpdata doesn't insert newlines, and KiCad chokes when there's one long line - modified_string = modified_string.replace('))', '))\n') - args.output_pcb.write(modified_string) - print("Wrote PCB") + modified_string = sexpdata.dumps(pcb_sexp) + # sexpdata doesn't insert newlines, and KiCad chokes when there's one long line + modified_string = modified_string.replace("))", "))\n") + args.output_pcb.write(modified_string) + print("Wrote PCB") diff --git a/examples/SevenSegment/resources/rename_parts.py b/examples/SevenSegment/resources/rename_parts.py index 60631c8a5..53133e0d1 100644 --- a/examples/SevenSegment/resources/rename_parts.py +++ b/examples/SevenSegment/resources/rename_parts.py @@ -15,42 +15,42 @@ # (first component matching the original name takes the first name in the rename list, # second takes the second name, and so on) remap_table: Dict[str, List[str]] = {} -remap_table['led'] = [] +remap_table["led"] = [] for digit in range(4): - for segment, segment_name in enumerate(['a', 'b', 'c', 'd', 'e', 'f', 'g']): - for led in range(2): - led_id = segment * 2 + led - remap_table['led'].append(f'digit[{digit}].led[{led_id}]') -remap_table['sw'] = ['sw[0]', 'sw[1]', 'sw[2]', 'sw[3]'] -remap_table['led'].extend(['center.led[0]', 'center.led[1]']) -remap_table['led'].extend(['meta.led[0]', 'meta.led[1]', 'meta.led[3]', 'meta.led[4]']) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Rename refdeses in a PCB.') - parser.add_argument('input_pcb', type=argparse.FileType('r')) - parser.add_argument('output_pcb', type=argparse.FileType('w')) - args = parser.parse_args() - - pcb_data = args.input_pcb.read() - pcb_sexp = sexpdata.loads(pcb_data) - assert parse_symbol(pcb_sexp[0]) == 'kicad_pcb' - pcb_dict = group_by_car(pcb_sexp) - for footprint in pcb_dict['footprint']: - footprint_dict = group_by_car(footprint) - for fp_text in footprint_dict['fp_text']: - if fp_text[1] == sexpdata.Symbol('reference'): - old_refdes = fp_text[2] - if old_refdes in remap_table: - remap_list = remap_table[old_refdes] - if remap_list: - fp_text[2] = remap_list.pop(0) - print(f"rename {old_refdes} => {fp_text[2]}") - else: - print(f"rename {old_refdes} failed, empty list") - - modified_string = sexpdata.dumps(pcb_sexp) - # sexpdata doesn't insert newlines, and KiCad chokes when there's one long line - modified_string = modified_string.replace('))', '))\n') - args.output_pcb.write(modified_string) - print("Wrote PCB") + for segment, segment_name in enumerate(["a", "b", "c", "d", "e", "f", "g"]): + for led in range(2): + led_id = segment * 2 + led + remap_table["led"].append(f"digit[{digit}].led[{led_id}]") +remap_table["sw"] = ["sw[0]", "sw[1]", "sw[2]", "sw[3]"] +remap_table["led"].extend(["center.led[0]", "center.led[1]"]) +remap_table["led"].extend(["meta.led[0]", "meta.led[1]", "meta.led[3]", "meta.led[4]"]) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Rename refdeses in a PCB.") + parser.add_argument("input_pcb", type=argparse.FileType("r")) + parser.add_argument("output_pcb", type=argparse.FileType("w")) + args = parser.parse_args() + + pcb_data = args.input_pcb.read() + pcb_sexp = sexpdata.loads(pcb_data) + assert parse_symbol(pcb_sexp[0]) == "kicad_pcb" + pcb_dict = group_by_car(pcb_sexp) + for footprint in pcb_dict["footprint"]: + footprint_dict = group_by_car(footprint) + for fp_text in footprint_dict["fp_text"]: + if fp_text[1] == sexpdata.Symbol("reference"): + old_refdes = fp_text[2] + if old_refdes in remap_table: + remap_list = remap_table[old_refdes] + if remap_list: + fp_text[2] = remap_list.pop(0) + print(f"rename {old_refdes} => {fp_text[2]}") + else: + print(f"rename {old_refdes} failed, empty list") + + modified_string = sexpdata.dumps(pcb_sexp) + # sexpdata doesn't insert newlines, and KiCad chokes when there's one long line + modified_string = modified_string.replace("))", "))\n") + args.output_pcb.write(modified_string) + print("Wrote PCB") diff --git a/examples/jlcpcb_pcba_postprocess.py b/examples/jlcpcb_pcba_postprocess.py index ce321390f..a9d185128 100644 --- a/examples/jlcpcb_pcba_postprocess.py +++ b/examples/jlcpcb_pcba_postprocess.py @@ -4,227 +4,221 @@ import os from typing import Dict, Tuple, Optional -parser = argparse.ArgumentParser(description='Post-process KiCad BoM and position files to be compatible with JLC.') -parser.add_argument('file_path_prefix', type=str, - help='Path prefix to the part data, without the .csv or -top-post.csv postfix, ' + - 'for example LedMatrix/gerbers/LedMatrix') -parser.add_argument('--merge-boms', type=str, nargs='*', - help="BoM CSVs to merge, for panelization. " + - "If specified, replaces the BoM CSV in the file_path_prefix.") +parser = argparse.ArgumentParser(description="Post-process KiCad BoM and position files to be compatible with JLC.") +parser.add_argument( + "file_path_prefix", + type=str, + help="Path prefix to the part data, without the .csv or -top-post.csv postfix, " + + "for example LedMatrix/gerbers/LedMatrix", +) +parser.add_argument( + "--merge-boms", + type=str, + nargs="*", + help="BoM CSVs to merge, for panelization. " + "If specified, replaces the BoM CSV in the file_path_prefix.", +) args = parser.parse_args() # Correct the rotations on a per-part-number-basis PART_ROTATIONS = { - 'C425057': -90, # resistor array 750ohm 4x0603 - 'C20197': -90, # resistor array 1k 4x0603 - 'C8734': -90, # STM32F103C8T6 - 'C432211': 90, # STM32G031GBU6 - 'C91199': 180, # VL53L0X - 'C27396': -90, # TPA2005D1 - 'C12084': -90, # SN65HVD230DR - 'C264517': 180, # 0606 RGB LED - 'C158099': 90, # 0404 RGB LED - 'C86832': -90, # PCF8574 IO expander - 'C500769': -90, # AP3418 buck converter - 'C50506': -90, # DRV8833 dual motor driver - 'C92482': -90, # DRV8313 BLDC driver - 'C132291': -90, # FUSB302B - 'C508453': 180, # FET - 'C527684': -90, # TPS54202H SOT-23-6 - 'C155534': -90, # AL8861 MSOP-8 - 'C7722': -90, # TPS61040 SOT-23-5 - 'C216623': 0, # XC6209 1.5 SOT-23-5 - 'C216624': 0, # XC6209 3.3 SOT-23-5 - 'C222571': 0, # XC6209 5.0 SOT-23-5 - 'C86803': -90, # PCA9554A TSSOP16 - 'C106912': -90, # NLAS4157 - 'C3663690': 180, # TMP1075N - 'C70285': 180, # SN74LVC1G74DCUR - 'C2651906': -90, # DG468 - 'C840095': 0, # LM2664 SOT-23-6 - 'C2858491': 270, # ESP32-C3H4 - seems really broken - 'C2865497': 0, # TPS92200D2DDCR SOT-23-6 - 'C7976': 180, # LMV331IDCKR SOT-353 - 'C478093': -90, # MCP4728T-E/UN - - 'C2962219': -90, # 2x5 1.27mm header shrouded - 'C126830': 90, # "SOT-23" USB ESD protector - 'C6568': -90, # CP2102 USB UART - 'C976032': -90, # LGA-16 QMC5883L - 'C3303790': -90, # PN7160 QFN - 'C191341': -90, # SX1262 - 'C470892': -90, # PE4259 - - 'C650309': -90, # AD5941 - - 'C424093': -90, # MCP73831T - 'C842287': -90, # 74AHCT1G125 - 'C113367': -90, # PAM8302 - 'C2890035': -90, # SK6805-EC15 - 'C125972': 90, # BME680 - 'C2682619': 180, # MAX98357 BGA - 'C910544': -90, # MAX98357 QFN - - 'C262645': 180, # AFC07 FPC 30 - 'C262669': 180, # AFC01 FPC 24 - - 'C424662': -90, # FH35C 0.3mm FPC 31 - - 'C209762': -90, # EC11J15 - - 'C585350': 180, # Molex microSD 104031-0811 + "C425057": -90, # resistor array 750ohm 4x0603 + "C20197": -90, # resistor array 1k 4x0603 + "C8734": -90, # STM32F103C8T6 + "C432211": 90, # STM32G031GBU6 + "C91199": 180, # VL53L0X + "C27396": -90, # TPA2005D1 + "C12084": -90, # SN65HVD230DR + "C264517": 180, # 0606 RGB LED + "C158099": 90, # 0404 RGB LED + "C86832": -90, # PCF8574 IO expander + "C500769": -90, # AP3418 buck converter + "C50506": -90, # DRV8833 dual motor driver + "C92482": -90, # DRV8313 BLDC driver + "C132291": -90, # FUSB302B + "C508453": 180, # FET + "C527684": -90, # TPS54202H SOT-23-6 + "C155534": -90, # AL8861 MSOP-8 + "C7722": -90, # TPS61040 SOT-23-5 + "C216623": 0, # XC6209 1.5 SOT-23-5 + "C216624": 0, # XC6209 3.3 SOT-23-5 + "C222571": 0, # XC6209 5.0 SOT-23-5 + "C86803": -90, # PCA9554A TSSOP16 + "C106912": -90, # NLAS4157 + "C3663690": 180, # TMP1075N + "C70285": 180, # SN74LVC1G74DCUR + "C2651906": -90, # DG468 + "C840095": 0, # LM2664 SOT-23-6 + "C2858491": 270, # ESP32-C3H4 - seems really broken + "C2865497": 0, # TPS92200D2DDCR SOT-23-6 + "C7976": 180, # LMV331IDCKR SOT-353 + "C478093": -90, # MCP4728T-E/UN + "C2962219": -90, # 2x5 1.27mm header shrouded + "C126830": 90, # "SOT-23" USB ESD protector + "C6568": -90, # CP2102 USB UART + "C976032": -90, # LGA-16 QMC5883L + "C3303790": -90, # PN7160 QFN + "C191341": -90, # SX1262 + "C470892": -90, # PE4259 + "C650309": -90, # AD5941 + "C424093": -90, # MCP73831T + "C842287": -90, # 74AHCT1G125 + "C113367": -90, # PAM8302 + "C2890035": -90, # SK6805-EC15 + "C125972": 90, # BME680 + "C2682619": 180, # MAX98357 BGA + "C910544": -90, # MAX98357 QFN + "C262645": 180, # AFC07 FPC 30 + "C262669": 180, # AFC01 FPC 24 + "C424662": -90, # FH35C 0.3mm FPC 31 + "C209762": -90, # EC11J15 + "C585350": 180, # Molex microSD 104031-0811 } _FOOTPRINT_ROTATIONS = { - 'Connector_USB:USB_C_Receptacle_XKB_U262-16XN-4BVC11': 0, - 'RF_Module:ESP32-WROOM-32': -90, - 'Package_TO_SOT_SMD:SOT-23': 180, - 'Package_TO_SOT_SMD:SOT-23-5': 180, - 'Package_TO_SOT_SMD:SOT-23-6': 180, - 'Package_TO_SOT_SMD:SOT-23-8': 180, - 'Package_TO_SOT_SMD:SOT-89-3': 180, - 'Package_TO_SOT_SMD:SOT-323_SC-70': 180, - 'Package_TO_SOT_SMD:SOT-363_SC-70-6': 180, - 'Package_TO_SOT_SMD:SOT-223-3_TabPin2': 180, - 'Package_TO_SOT_SMD:TO-252-2': 180, - 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm': -90, - 'Package_SO:SOIC-8_5.23x5.23mm_P1.27mm': -90, - 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic': -90, - 'Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm': -90, - - 'OptoDevice:Osram_BPW34S-SMD': 180, - - 'Connector_Coaxial:BNC_Amphenol_B6252HB-NPP3G-50_Horizontal': 180, - - 'Connector_JST:JST_PH_S2B-PH-K_1x02_P2.00mm_Horizontal': 180, - - # note, SMD e-cap are sometimes flipped but are not included here as it's inconsistent + "Connector_USB:USB_C_Receptacle_XKB_U262-16XN-4BVC11": 0, + "RF_Module:ESP32-WROOM-32": -90, + "Package_TO_SOT_SMD:SOT-23": 180, + "Package_TO_SOT_SMD:SOT-23-5": 180, + "Package_TO_SOT_SMD:SOT-23-6": 180, + "Package_TO_SOT_SMD:SOT-23-8": 180, + "Package_TO_SOT_SMD:SOT-89-3": 180, + "Package_TO_SOT_SMD:SOT-323_SC-70": 180, + "Package_TO_SOT_SMD:SOT-363_SC-70-6": 180, + "Package_TO_SOT_SMD:SOT-223-3_TabPin2": 180, + "Package_TO_SOT_SMD:TO-252-2": 180, + "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm": -90, + "Package_SO:SOIC-8_5.23x5.23mm_P1.27mm": -90, + "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic": -90, + "Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm": -90, + "OptoDevice:Osram_BPW34S-SMD": 180, + "Connector_Coaxial:BNC_Amphenol_B6252HB-NPP3G-50_Horizontal": 180, + "Connector_JST:JST_PH_S2B-PH-K_1x02_P2.00mm_Horizontal": 180, + # note, SMD e-cap are sometimes flipped but are not included here as it's inconsistent } # footprint position export doesn't include the footprint library name -PACKAGE_ROTATIONS = {footprint.split(':')[-1]: rot for footprint, rot in _FOOTPRINT_ROTATIONS.items()} +PACKAGE_ROTATIONS = {footprint.split(":")[-1]: rot for footprint, rot in _FOOTPRINT_ROTATIONS.items()} # translational offsets using KiCad coordinate conventions, -y is up # offsets estimated visually PART_OFFSETS: Dict[str, Tuple[float, float]] = { - 'C262669': (0, -0.5), # AFC01 FPC 24 - 'C262671': (0, -0.5), # AFC01 FPC 30 - 'C262643': (0, -0.7), # AFC07 FPC 24 - 'C262645': (0, -0.7), # AFC07 FPC 30 - 'C110293': (0, 0.1), # SKRTLAE010 R/A switch - 'C116648': (0, 2.1), # EC05E1220401 - - 'C496552': (-0.3, 0), # BWIPX-1-001E U.FL connector + "C262669": (0, -0.5), # AFC01 FPC 24 + "C262671": (0, -0.5), # AFC01 FPC 30 + "C262643": (0, -0.7), # AFC07 FPC 24 + "C262645": (0, -0.7), # AFC07 FPC 30 + "C110293": (0, 0.1), # SKRTLAE010 R/A switch + "C116648": (0, 2.1), # EC05E1220401 + "C496552": (-0.3, 0), # BWIPX-1-001E U.FL connector } _FOOTPRINT_OFFSETS: Dict[str, Tuple[float, float]] = { - 'Package_TO_SOT_SMD:SOT-89-3': (-0.6, 0), - 'Package_TO_SOT_SMD:TO-252-2': (-2, 0), - - 'Connector_USB:USB_C_Receptacle_XKB_U262-16XN-4BVC11': (0, -1.25), - 'RF_Module:ESP32-WROOM-32': (0, 0.8), - - 'Connector_Coaxial:BNC_Amphenol_B6252HB-NPP3G-50_Horizontal': (0, -2.5), - - 'Connector_JST:JST_PH_S2B-PH-K_1x02_P2.00mm_Horizontal': (1, 0), - 'Connector_JST:JST_PH_S6B-PH-K_1x06_P2.00mm_Horizontal': (5, 0), + "Package_TO_SOT_SMD:SOT-89-3": (-0.6, 0), + "Package_TO_SOT_SMD:TO-252-2": (-2, 0), + "Connector_USB:USB_C_Receptacle_XKB_U262-16XN-4BVC11": (0, -1.25), + "RF_Module:ESP32-WROOM-32": (0, 0.8), + "Connector_Coaxial:BNC_Amphenol_B6252HB-NPP3G-50_Horizontal": (0, -2.5), + "Connector_JST:JST_PH_S2B-PH-K_1x02_P2.00mm_Horizontal": (1, 0), + "Connector_JST:JST_PH_S6B-PH-K_1x06_P2.00mm_Horizontal": (5, 0), } -PACKAGE_OFFSETS = {footprint.split(':')[-1]: offset for footprint, offset in _FOOTPRINT_OFFSETS.items()} - -if __name__ == '__main__': - def remap_by_dict(elt: str, remap_dict: Dict[str, str]) -> str: - if elt in remap_dict: - return remap_dict[elt] - else: - return elt - - refdes_lcsc_map: Dict[str, str] = {} # refdes -> LCSC part number - - if args.merge_boms: - if os.path.exists(f'{args.file_path_prefix}.csv'): # remove previous one to avoid confusion - os.remove(f'{args.file_path_prefix}.csv') - with open(f'{args.file_path_prefix}.csv', 'w', newline='') as bom_out: - merged_csv_out: Optional[csv.DictWriter[str]] = None - for input_bom_file in args.merge_boms: - with open(input_bom_file, 'r', newline='') as bom_in: - csv_dict_in = csv.DictReader(bom_in) - if merged_csv_out is None: - assert csv_dict_in.fieldnames is not None - merged_csv_out = csv.DictWriter(bom_out, fieldnames=csv_dict_in.fieldnames) - merged_csv_out.writeheader() - for row_dict in csv_dict_in: - merged_csv_out.writerow(row_dict) - - # while we don't need to modify this file, we do need the JLC P/N to refdes map - # to apply the rotations, since that data isn't in the placement file - with open(f'{args.file_path_prefix}.csv', 'r', newline='') as bom_in: - csv_in = csv.reader(bom_in) - - rows = list(csv_in) - refdes_list_index = rows[0].index('Designator') - lcsc_index = rows[0].index('JLCPCB Part #') - - for i, row in enumerate(rows[1:]): - if not row[lcsc_index]: # ignore rows without part number - continue - refdes_list = row[refdes_list_index].split(',') - for refdes in refdes_list: - assert refdes not in refdes_lcsc_map, f"duplicate refdes {refdes} in row {i+1}" - refdes_lcsc_map[refdes] = row[lcsc_index] - - print(f"read {args.file_path_prefix}.csv") - - # Process position CSV - POS_HEADER_MAP = { - 'Ref': 'Designator', - 'PosX': 'Mid X', - 'PosY': 'Mid Y', - 'Rot': 'Rotation', - 'Side': 'Layer', - } - for pos_postfix in ['top', 'bottom']: - with open(f'{args.file_path_prefix}-{pos_postfix}-pos.csv', 'r', newline='') as pos_in, \ - open(f'{args.file_path_prefix}-{pos_postfix}-pos_jlc.csv', 'w', newline='') as pos_out: - csv_in = csv.reader(pos_in) - csv_out = csv.writer(pos_out) - - rows = list(csv_in) - rows[0] = [remap_by_dict(elt, POS_HEADER_MAP) for elt in rows[0]] - - refdes_index = rows[0].index('Designator') - package_index = rows[0].index('Package') - rot_index = rows[0].index('Rotation') - x_index = rows[0].index('Mid X') - y_index = rows[0].index('Mid Y') - - csv_out.writerow(rows[0]) - - for i, row in enumerate(rows[1:]): - refdes = row[refdes_index] - package = row[package_index] - lcsc_opt = refdes_lcsc_map.get(refdes, None) - - # correct offsets before applying rotation - if lcsc_opt is not None and lcsc_opt in PART_OFFSETS: - (xoff, yoff) = PART_OFFSETS[lcsc_opt] - rot = math.radians(float(row[rot_index])) - row[x_index] = str((float(row[x_index]) + xoff * math.cos(rot) + yoff * math.sin(rot))) - row[y_index] = str((float(row[y_index]) + xoff * math.sin(rot) - yoff * math.cos(rot))) - print(f"correct offset for row {i+1} ref {refdes}, {lcsc_opt}") - elif package in PACKAGE_OFFSETS: - (xoff, yoff) = PACKAGE_OFFSETS[package] - rot = math.radians(float(row[rot_index])) - row[x_index] = str((float(row[x_index]) + xoff * math.cos(rot) + yoff * math.sin(rot))) - row[y_index] = str((float(row[y_index]) + xoff * math.sin(rot) - yoff * math.cos(rot))) - print(f"correct offset for row {i+1} ref {refdes}, {package}") - - if lcsc_opt is not None and lcsc_opt in PART_ROTATIONS: - row[rot_index] = str((float(row[rot_index]) + PART_ROTATIONS[lcsc_opt]) % 360) - print(f"correct rotation for row {i+1} ref {refdes}, {lcsc_opt}") - elif package in PACKAGE_ROTATIONS: - row[rot_index] = str((float(row[rot_index]) + PACKAGE_ROTATIONS[package]) % 360) - print(f"correct rotation for row {i+1} ref {refdes}, {package}") - - csv_out.writerow(row) - - print(f"wrote {args.file_path_prefix}-{pos_postfix}-pos_jlc.csv") +PACKAGE_OFFSETS = {footprint.split(":")[-1]: offset for footprint, offset in _FOOTPRINT_OFFSETS.items()} + +if __name__ == "__main__": + + def remap_by_dict(elt: str, remap_dict: Dict[str, str]) -> str: + if elt in remap_dict: + return remap_dict[elt] + else: + return elt + + refdes_lcsc_map: Dict[str, str] = {} # refdes -> LCSC part number + + if args.merge_boms: + if os.path.exists(f"{args.file_path_prefix}.csv"): # remove previous one to avoid confusion + os.remove(f"{args.file_path_prefix}.csv") + with open(f"{args.file_path_prefix}.csv", "w", newline="") as bom_out: + merged_csv_out: Optional[csv.DictWriter[str]] = None + for input_bom_file in args.merge_boms: + with open(input_bom_file, "r", newline="") as bom_in: + csv_dict_in = csv.DictReader(bom_in) + if merged_csv_out is None: + assert csv_dict_in.fieldnames is not None + merged_csv_out = csv.DictWriter(bom_out, fieldnames=csv_dict_in.fieldnames) + merged_csv_out.writeheader() + for row_dict in csv_dict_in: + merged_csv_out.writerow(row_dict) + + # while we don't need to modify this file, we do need the JLC P/N to refdes map + # to apply the rotations, since that data isn't in the placement file + with open(f"{args.file_path_prefix}.csv", "r", newline="") as bom_in: + csv_in = csv.reader(bom_in) + + rows = list(csv_in) + refdes_list_index = rows[0].index("Designator") + lcsc_index = rows[0].index("JLCPCB Part #") + + for i, row in enumerate(rows[1:]): + if not row[lcsc_index]: # ignore rows without part number + continue + refdes_list = row[refdes_list_index].split(",") + for refdes in refdes_list: + assert refdes not in refdes_lcsc_map, f"duplicate refdes {refdes} in row {i+1}" + refdes_lcsc_map[refdes] = row[lcsc_index] + + print(f"read {args.file_path_prefix}.csv") + + # Process position CSV + POS_HEADER_MAP = { + "Ref": "Designator", + "PosX": "Mid X", + "PosY": "Mid Y", + "Rot": "Rotation", + "Side": "Layer", + } + for pos_postfix in ["top", "bottom"]: + with ( + open(f"{args.file_path_prefix}-{pos_postfix}-pos.csv", "r", newline="") as pos_in, + open(f"{args.file_path_prefix}-{pos_postfix}-pos_jlc.csv", "w", newline="") as pos_out, + ): + csv_in = csv.reader(pos_in) + csv_out = csv.writer(pos_out) + + rows = list(csv_in) + rows[0] = [remap_by_dict(elt, POS_HEADER_MAP) for elt in rows[0]] + + refdes_index = rows[0].index("Designator") + package_index = rows[0].index("Package") + rot_index = rows[0].index("Rotation") + x_index = rows[0].index("Mid X") + y_index = rows[0].index("Mid Y") + + csv_out.writerow(rows[0]) + + for i, row in enumerate(rows[1:]): + refdes = row[refdes_index] + package = row[package_index] + lcsc_opt = refdes_lcsc_map.get(refdes, None) + + # correct offsets before applying rotation + if lcsc_opt is not None and lcsc_opt in PART_OFFSETS: + (xoff, yoff) = PART_OFFSETS[lcsc_opt] + rot = math.radians(float(row[rot_index])) + row[x_index] = str((float(row[x_index]) + xoff * math.cos(rot) + yoff * math.sin(rot))) + row[y_index] = str((float(row[y_index]) + xoff * math.sin(rot) - yoff * math.cos(rot))) + print(f"correct offset for row {i+1} ref {refdes}, {lcsc_opt}") + elif package in PACKAGE_OFFSETS: + (xoff, yoff) = PACKAGE_OFFSETS[package] + rot = math.radians(float(row[rot_index])) + row[x_index] = str((float(row[x_index]) + xoff * math.cos(rot) + yoff * math.sin(rot))) + row[y_index] = str((float(row[y_index]) + xoff * math.sin(rot) - yoff * math.cos(rot))) + print(f"correct offset for row {i+1} ref {refdes}, {package}") + + if lcsc_opt is not None and lcsc_opt in PART_ROTATIONS: + row[rot_index] = str((float(row[rot_index]) + PART_ROTATIONS[lcsc_opt]) % 360) + print(f"correct rotation for row {i+1} ref {refdes}, {lcsc_opt}") + elif package in PACKAGE_ROTATIONS: + row[rot_index] = str((float(row[rot_index]) + PACKAGE_ROTATIONS[package]) % 360) + print(f"correct rotation for row {i+1} ref {refdes}, {package}") + + csv_out.writerow(row) + + print(f"wrote {args.file_path_prefix}-{pos_postfix}-pos_jlc.csv") diff --git a/examples/resources/upgrade_pcb.py b/examples/resources/upgrade_pcb.py index 77ae28062..103ee3817 100644 --- a/examples/resources/upgrade_pcb.py +++ b/examples/resources/upgrade_pcb.py @@ -7,68 +7,69 @@ from edg.electronics_model.KiCadSchematicParser import group_by_car, parse_symbol, test_cast -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Update timestamps on a kicad_pcb file from a new netlist.' - + ' This is only used to upgrade a KiCad 5 PCB for use in KiCad 6,' - + ' since KiCad 6 does not support non-numeric timestamps which netlists use.' - + ' You may need to pre-process the kicad_pcb file to update module paths.') - parser.add_argument('input_pcb', type=argparse.FileType('r')) - parser.add_argument('input_netlist', type=argparse.FileType('r')) - parser.add_argument('output_pcb', type=argparse.FileType('w')) - args = parser.parse_args() +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Update timestamps on a kicad_pcb file from a new netlist." + + " This is only used to upgrade a KiCad 5 PCB for use in KiCad 6," + + " since KiCad 6 does not support non-numeric timestamps which netlists use." + + " You may need to pre-process the kicad_pcb file to update module paths." + ) + parser.add_argument("input_pcb", type=argparse.FileType("r")) + parser.add_argument("input_netlist", type=argparse.FileType("r")) + parser.add_argument("output_pcb", type=argparse.FileType("w")) + args = parser.parse_args() - # build pathname -> new timestamp mapping - path_timestamp_map: Dict[str, str] = {} + # build pathname -> new timestamp mapping + path_timestamp_map: Dict[str, str] = {} - net_sexp = sexpdata.loads(args.input_netlist.read()) - assert parse_symbol(net_sexp[0]) == 'export' - net_dict = group_by_car(net_sexp) + net_sexp = sexpdata.loads(args.input_netlist.read()) + assert parse_symbol(net_sexp[0]) == "export" + net_dict = group_by_car(net_sexp) - net_components = net_dict['components'] - assert len(net_components) == 1 - for elt in net_components[0][1:]: - assert parse_symbol(elt[0]) == 'comp' - elt_dict = group_by_car(elt) - path_tstamp = elt_dict['sheetpath'][0][2][1] - tstamp = elt_dict['tstamps'][0][1] - props = {test_cast(prop[1][1], str): test_cast(prop[2][1], str) - for prop in elt_dict.get('property', [])} - path = props['edg_short_path'] - path_timestamp_map['/' + path.replace('.', '/')] = path_tstamp + tstamp + net_components = net_dict["components"] + assert len(net_components) == 1 + for elt in net_components[0][1:]: + assert parse_symbol(elt[0]) == "comp" + elt_dict = group_by_car(elt) + path_tstamp = elt_dict["sheetpath"][0][2][1] + tstamp = elt_dict["tstamps"][0][1] + props = {test_cast(prop[1][1], str): test_cast(prop[2][1], str) for prop in elt_dict.get("property", [])} + path = props["edg_short_path"] + path_timestamp_map["/" + path.replace(".", "/")] = path_tstamp + tstamp - pcb_data = args.input_pcb.read() - pcb_sexp = sexpdata.loads(pcb_data) - assert parse_symbol(pcb_sexp[0]) == 'kicad_pcb' - pcb_dict = group_by_car(pcb_sexp) + pcb_data = args.input_pcb.read() + pcb_sexp = sexpdata.loads(pcb_data) + assert parse_symbol(pcb_sexp[0]) == "kicad_pcb" + pcb_dict = group_by_car(pcb_sexp) - # remapping is done on the string because sexpdata fails to roundtrip PCB files - new_pcb_data = pcb_data + # remapping is done on the string because sexpdata fails to roundtrip PCB files + new_pcb_data = pcb_data - net_unseen_paths: List[str] = list(path_timestamp_map.keys()) - pcb_unmapped_paths: List[str] = [] - for elt in pcb_dict['module']: - elt_dict = group_by_car(elt) - tstamp = elt_dict['tstamp'][0][1] - path_raw = elt_dict['path'][0][1] - if isinstance(path_raw, sexpdata.Symbol): - path = parse_symbol(path_raw) - else: - assert isinstance(path_raw, str) - path = path_raw + net_unseen_paths: List[str] = list(path_timestamp_map.keys()) + pcb_unmapped_paths: List[str] = [] + for elt in pcb_dict["module"]: + elt_dict = group_by_car(elt) + tstamp = elt_dict["tstamp"][0][1] + path_raw = elt_dict["path"][0][1] + if isinstance(path_raw, sexpdata.Symbol): + path = parse_symbol(path_raw) + else: + assert isinstance(path_raw, str) + path = path_raw - if path in path_timestamp_map: - # support quoted notation since symbol notation disallows [] array index - assert f'(path {path})' in new_pcb_data or f'(path "{path}")' in new_pcb_data - net_tstamp = path_timestamp_map[path] - new_pcb_data = new_pcb_data.replace(f'(path {path})', f'(path {net_tstamp})') - new_pcb_data = new_pcb_data.replace(f'(path "{path}")', f'(path {net_tstamp})') - net_unseen_paths.remove(path) - print(f"Map: {tstamp}, {path} => {net_tstamp}") - else: - pcb_unmapped_paths.append(path) + if path in path_timestamp_map: + # support quoted notation since symbol notation disallows [] array index + assert f"(path {path})" in new_pcb_data or f'(path "{path}")' in new_pcb_data + net_tstamp = path_timestamp_map[path] + new_pcb_data = new_pcb_data.replace(f"(path {path})", f"(path {net_tstamp})") + new_pcb_data = new_pcb_data.replace(f'(path "{path}")', f"(path {net_tstamp})") + net_unseen_paths.remove(path) + print(f"Map: {tstamp}, {path} => {net_tstamp}") + else: + pcb_unmapped_paths.append(path) - print(f"Netlist unseen paths: {net_unseen_paths}") - print(f"PCB unmapped paths: {pcb_unmapped_paths}") + print(f"Netlist unseen paths: {net_unseen_paths}") + print(f"PCB unmapped paths: {pcb_unmapped_paths}") - args.output_pcb.write(new_pcb_data) - print("Wrote PCB") + args.output_pcb.write(new_pcb_data) + print("Wrote PCB") diff --git a/examples/test_basickeyboard.py b/examples/test_basickeyboard.py index 554e393a0..cb8522041 100644 --- a/examples/test_basickeyboard.py +++ b/examples/test_basickeyboard.py @@ -16,25 +16,25 @@ class BasicKeyboard(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.mcu = self.Block(Xiao_Rp2040()) + self.mcu = self.Block(Xiao_Rp2040()) - self.sw = self.Block(SwitchMatrix(nrows=3, ncols=2)) - self.connect(self.sw.cols, self.mcu.gpio.request_vector()) - self.connect(self.sw.rows, self.mcu.gpio.request_vector()) + self.sw = self.Block(SwitchMatrix(nrows=3, ncols=2)) + self.connect(self.sw.cols, self.mcu.gpio.request_vector()) + self.connect(self.sw.rows, self.mcu.gpio.request_vector()) - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - class_refinements=[ - (Switch, KailhSocket), - ], - ) + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + class_refinements=[ + (Switch, KailhSocket), + ], + ) class BasicKeyboardTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(BasicKeyboard) + def test_design(self) -> None: + compile_board_inplace(BasicKeyboard) diff --git a/examples/test_battery_protector.py b/examples/test_battery_protector.py index 3e7889f38..f39656dc3 100644 --- a/examples/test_battery_protector.py +++ b/examples/test_battery_protector.py @@ -6,20 +6,20 @@ class BatteryProtectorCircuit(BoardTop): - @override - def contents(self) -> None: - super().contents() - self.battery_protector = self.Block(S8261A()) - self.li_ion_bat = self.Block(Li18650(voltage=(2.5, 4.2) * Volt)) - self.led = self.Block(VoltageIndicatorLed()) + @override + def contents(self) -> None: + super().contents() + self.battery_protector = self.Block(S8261A()) + self.li_ion_bat = self.Block(Li18650(voltage=(2.5, 4.2) * Volt)) + self.led = self.Block(VoltageIndicatorLed()) - self.link_bat_neg = self.connect(self.li_ion_bat.gnd, self.battery_protector.gnd_in) - self.link_bat_pos = self.connect(self.li_ion_bat.pwr, self.battery_protector.pwr_in) + self.link_bat_neg = self.connect(self.li_ion_bat.gnd, self.battery_protector.gnd_in) + self.link_bat_pos = self.connect(self.li_ion_bat.pwr, self.battery_protector.pwr_in) - self.link_protect_neg = self.connect(self.battery_protector.gnd_out, self.led.gnd) - self.link_protect_pos = self.connect(self.battery_protector.pwr_out, self.led.signal) + self.link_protect_neg = self.connect(self.battery_protector.gnd_out, self.led.gnd) + self.link_protect_pos = self.connect(self.battery_protector.pwr_out, self.led.signal) class BatteryProtectorCircuitTestCase(unittest.TestCase): - def test_design_battery_protector(self) -> None: - compile_board_inplace(BatteryProtectorCircuit, False) + def test_design_battery_protector(self) -> None: + compile_board_inplace(BatteryProtectorCircuit, False) diff --git a/examples/test_bldc_controller.py b/examples/test_bldc_controller.py index 7393c4ec3..9254daf97 100644 --- a/examples/test_bldc_controller.py +++ b/examples/test_bldc_controller.py @@ -6,229 +6,262 @@ class BldcConnector(Connector, Block): - """Parameterizable-current connector to an external BLDC motor.""" - def __init__(self, max_current: FloatLike): - super().__init__() - self.conn = self.Block(PassiveConnector()) - self.phases = self.Port(Vector(DigitalSink.empty())) + """Parameterizable-current connector to an external BLDC motor.""" - phase_model = DigitalSink( - current_draw=(-max_current, max_current) - ) - for i in ['1', '2', '3']: - phase_i = self.phases.append_elt(DigitalSink.empty(), i) - self.require(phase_i.is_connected(), f"all phases {i} must be connected") - self.connect(phase_i, self.conn.pins.request(i).adapt_to(phase_model)) + def __init__(self, max_current: FloatLike): + super().__init__() + self.conn = self.Block(PassiveConnector()) + self.phases = self.Port(Vector(DigitalSink.empty())) + + phase_model = DigitalSink(current_draw=(-max_current, max_current)) + for i in ["1", "2", "3"]: + phase_i = self.phases.append_elt(DigitalSink.empty(), i) + self.require(phase_i.is_connected(), f"all phases {i} must be connected") + self.connect(phase_i, self.conn.pins.request(i).adapt_to(phase_model)) class MagneticEncoder(Connector, Magnetometer, Block): - """Connector to AS5600 mangetic encoder, - https://ams.com/documents/20143/36005/AS5600_DS000365_5-00.pdf""" - def __init__(self) -> None: - super().__init__() - self.conn = self.Block(PassiveConnector()) - - self.pwr = self.Export(self.conn.pins.request('1').adapt_to(VoltageSink( - voltage_limits=(3.0, 5.5), # 3.0-3.6 for 3.3v mode, 4.5-5.5 for 5v mode - current_draw=(1.5, 6.5)*mAmp, # supply current LPM3-NOM, excluding burn-in - )), [Power]) - self.gnd = self.Export(self.conn.pins.request('3').adapt_to(Ground()), - [Common]) - self.out = self.Export(self.conn.pins.request('2').adapt_to(AnalogSource.from_supply( - self.gnd, self.pwr - )), [Output]) + """Connector to AS5600 mangetic encoder, + https://ams.com/documents/20143/36005/AS5600_DS000365_5-00.pdf""" + + def __init__(self) -> None: + super().__init__() + self.conn = self.Block(PassiveConnector()) + + self.pwr = self.Export( + self.conn.pins.request("1").adapt_to( + VoltageSink( + voltage_limits=(3.0, 5.5), # 3.0-3.6 for 3.3v mode, 4.5-5.5 for 5v mode + current_draw=(1.5, 6.5) * mAmp, # supply current LPM3-NOM, excluding burn-in + ) + ), + [Power], + ) + self.gnd = self.Export(self.conn.pins.request("3").adapt_to(Ground()), [Common]) + self.out = self.Export( + self.conn.pins.request("2").adapt_to(AnalogSource.from_supply(self.gnd, self.pwr)), [Output] + ) class I2cConnector(Connector, Block): - """Generic I2C connector, QWIIC pinning (gnd/vcc/sda/scl)""" - def __init__(self) -> None: - super().__init__() - self.conn = self.Block(PassiveConnector()) - - self.gnd = self.Export(self.conn.pins.request('1').adapt_to(Ground()), - [Common]) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSink()), - [Power]) + """Generic I2C connector, QWIIC pinning (gnd/vcc/sda/scl)""" - self.i2c = self.Port(I2cTarget(DigitalBidir.empty()), [InOut]) - self.connect(self.i2c.sda, self.conn.pins.request('3').adapt_to(DigitalBidir())) - self.connect(self.i2c.scl, self.conn.pins.request('4').adapt_to(DigitalBidir())) + def __init__(self) -> None: + super().__init__() + self.conn = self.Block(PassiveConnector()) + self.gnd = self.Export(self.conn.pins.request("1").adapt_to(Ground()), [Common]) + self.pwr = self.Export(self.conn.pins.request("2").adapt_to(VoltageSink()), [Power]) -class BldcHallSensor(Connector, Block): - """Generic BLDC hall sensor, as +5v, U, V, W, GND""" - def __init__(self) -> None: - super().__init__() - self.conn = self.Block(PassiveConnector()) + self.i2c = self.Port(I2cTarget(DigitalBidir.empty()), [InOut]) + self.connect(self.i2c.sda, self.conn.pins.request("3").adapt_to(DigitalBidir())) + self.connect(self.i2c.scl, self.conn.pins.request("4").adapt_to(DigitalBidir())) - self.pwr = self.Export(self.conn.pins.request('1').adapt_to(VoltageSink( - voltage_limits=5*Volt(tol=0.1), - )), [Power]) - self.gnd = self.Export(self.conn.pins.request('5').adapt_to(Ground()), - [Common]) - self.phases = self.Port(Vector(DigitalSource.empty())) - phase_model = DigitalSource.low_from_supply(self.gnd) - for (pin, name) in [('2', 'u'), ('3', 'v'), ('4', 'w')]: - phase = self.phases.append_elt(DigitalSource.empty(), name) - self.require(phase.is_connected(), f"all phases {name} must be connected") - self.connect(phase, self.conn.pins.request(pin).adapt_to(phase_model)) +class BldcHallSensor(Connector, Block): + """Generic BLDC hall sensor, as +5v, U, V, W, GND""" + + def __init__(self) -> None: + super().__init__() + self.conn = self.Block(PassiveConnector()) + + self.pwr = self.Export( + self.conn.pins.request("1").adapt_to( + VoltageSink( + voltage_limits=5 * Volt(tol=0.1), + ) + ), + [Power], + ) + self.gnd = self.Export(self.conn.pins.request("5").adapt_to(Ground()), [Common]) + + self.phases = self.Port(Vector(DigitalSource.empty())) + phase_model = DigitalSource.low_from_supply(self.gnd) + for pin, name in [("2", "u"), ("3", "v"), ("4", "w")]: + phase = self.phases.append_elt(DigitalSource.empty(), name) + self.require(phase.is_connected(), f"all phases {name} must be connected") + self.connect(phase, self.conn.pins.request(pin).adapt_to(phase_model)) class BldcController(JlcBoardTop): - """Test BLDC (brushless DC motor) driver circuit with position feedback and USB PD - """ - @override - def contents(self) -> None: - super().contents() - - self.mcu = self.Block(IoController()) - mcu_pwr = self.mcu.with_mixin(IoControllerPowerOut()) - mcu_usb = self.mcu.with_mixin(IoControllerUsbOut()) - - self.motor_pwr = self.Block(LipoConnector(voltage=(2.5, 4.2)*Volt*6, actual_voltage=(2.5, 4.2)*Volt*6)) - - self.vusb = self.connect(mcu_usb.vusb_out) - self.v3v3 = self.connect(mcu_pwr.pwr_out) - self.gnd = self.connect(self.mcu.gnd, self.motor_pwr.gnd) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.sw1, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw1')) - (self.ledr, ), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request('ledr')) - (self.ledg, ), _ = self.chain(imp.Block(IndicatorLed(Led.Green)), self.mcu.gpio.request('ledg')) - (self.ledb, ), _ = self.chain(imp.Block(IndicatorLed(Led.Blue)), self.mcu.gpio.request('ledb')) - - i2c_bus = self.mcu.i2c.request('i2c') - (self.i2c_pull, self.i2c_tp, self.i2c), _ = self.chain( - i2c_bus, imp.Block(I2cPullup()), imp.Block(I2cTestPoint()), imp.Block(I2cConnector())) - - (self.ref_div, self.ref_buf, self.ref_tp), _ = self.chain( - self.v3v3, - imp.Block(VoltageDivider(output_voltage=1.5*Volt(tol=0.05), impedance=(10, 100)*kOhm)), - imp.Block(OpampFollower()), - self.Block(AnalogTestPoint()) - ) - self.vref = self.ref_buf.output - - # HALL SENSOR - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.hall = imp.Block(BldcHallSensor()) - self.connect(self.vusb, self.hall.pwr) - - (self.hall_pull, self.hall_tp), _ = self.chain(self.hall.phases, - self.Block(PullupResistorArray(4.7*kOhm(tol=0.05))), - self.Block(DigitalArrayTestPoint()), - self.mcu.gpio.request_vector('hall')) - self.connect(self.hall_pull.pwr, self.v3v3) - - # BLDC CONTROLLER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.vsense = imp.Block(VoltageSenseDivider(full_scale_voltage=(3.0, 3.3)*Volt, - impedance=10*kOhm(tol=0.2))) - self.connect(self.motor_pwr.pwr, self.vsense.input) - (self.vsense_tp, ), _ = self.chain(self.vsense.output, self.Block(AnalogTestPoint()), self.mcu.adc.request('vsense')) - - self.isense = imp.Block(OpampCurrentSensor( - resistance=0.05*Ohm(tol=0.01), - ratio=Range.from_tolerance(10, 0.05), input_impedance=10*kOhm(tol=0.05) - )) - self.connect(self.motor_pwr.pwr, self.isense.pwr_in, self.isense.pwr) - self.connect(self.isense.ref, self.vref) - (self.isense_tp, self.isense_clamp), _ = self.chain( - self.isense.out, - self.Block(AnalogTestPoint()), - imp.Block(AnalogClampResistor()), - self.mcu.adc.request('isense')) - - self.bldc_drv = imp.Block(Drv8313(risense_res=50*mOhm(tol=0.05))) - self.connect(self.isense.pwr_out, self.bldc_drv.pwr) - - self.connect(self.mcu.gpio.request('bldc_reset'), self.bldc_drv.nreset) - (self.bldc_fault_tp, ), _ = self.chain(self.mcu.gpio.request('bldc_fault'), - self.Block(DigitalTestPoint()), - self.bldc_drv.nfault) - (self.bldc_en_tp, ), _ = self.chain(self.mcu.gpio.request_vector('bldc_en'), - self.Block(DigitalArrayTestPoint()), - self.bldc_drv.ens) - (self.bldc_in_tp, ), _ = self.chain(self.mcu.gpio.request_vector('bldc_in'), - self.Block(DigitalArrayTestPoint()), - self.bldc_drv.ins) - - self.bldc = imp.Block(BldcConnector(2.5 * Amp)) # maximum of DRV8313 - self.connect(self.bldc_drv.outs.request_vector(), self.bldc.phases) - - self.curr = ElementDict[CurrentSenseResistor]() - self.curr_amp = ElementDict[Amplifier]() - self.curr_tp = ElementDict[AnalogTestPoint]() - for i in ['1', '2', '3']: - self.curr_amp[i] = imp.Block(Amplifier(Range.from_tolerance(20, 0.05))) - self.connect(self.curr_amp[i].pwr, self.v3v3) - (_, self.curr_tp[i], ), _ = self.chain(self.bldc_drv.pgnd_sense.request(i), self.curr_amp[i], - self.Block(AnalogTestPoint()), - self.mcu.adc.request(f'curr_{i}')) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Feather_Nrf52840), - (['isense', 'amp', 'amp'], Opa197) - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'ledb=3', - 'isense=5', - 'vsense=6', - 'ledg=7', - 'curr_3=8', - 'curr_2=9', - 'curr_1=10', - 'bldc_in_1=11', - 'bldc_en_1=12', - 'bldc_in_2=13', - 'bldc_en_2=14', - 'bldc_in_3=15', - 'ledr=16', - 'bldc_en_3=17', - 'bldc_reset=18', - 'bldc_fault=19', - 'sw1=20', - 'i2c.sda=21', - 'i2c.scl=22', - 'hall_u=23', - 'hall_v=24', - 'hall_w=25', - ]), - (['isense', 'sense', 'res', 'res', 'require_basic_part'], False), - (['bldc_drv', 'pgnd_res[1]', 'res', 'res', 'require_basic_part'], False), - (['bldc_drv', 'pgnd_res[1]', 'res', 'res', 'footprint_spec'], 'Resistor_SMD:R_2512_6332Metric'), - (['bldc_drv', 'pgnd_res[2]', 'res', 'res', 'require_basic_part'], ParamValue(['bldc_drv', 'pgnd_res[1]', 'res', 'res', 'require_basic_part'])), - (['bldc_drv', 'pgnd_res[2]', 'res', 'res', 'footprint_spec'], ParamValue(['bldc_drv', 'pgnd_res[1]', 'res', 'res', 'footprint_spec'])), - (['bldc_drv', 'pgnd_res[3]', 'res', 'res', 'require_basic_part'], ParamValue(['bldc_drv', 'pgnd_res[1]', 'res', 'res', 'require_basic_part'])), - (['bldc_drv', 'pgnd_res[3]', 'res', 'res', 'footprint_spec'], ParamValue(['bldc_drv', 'pgnd_res[1]', 'res', 'res', 'footprint_spec'])), - - (["bldc_drv", "vm_cap_bulk", "cap", "voltage_rating_derating"], 0.6), # allow using a 50V cap - (["bldc_drv", "cp_cap", "voltage_rating_derating"], 0.6), # allow using a 50V cap - - (["hall", "pwr", "voltage_limits"], Range(4, 5.5)), # allow with the Feather Vbus diode drop - ], - class_refinements=[ - (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified - (TestPoint, CompactKeystone5015), - ], - class_values=[ - ], - ) + """Test BLDC (brushless DC motor) driver circuit with position feedback and USB PD""" + + @override + def contents(self) -> None: + super().contents() + + self.mcu = self.Block(IoController()) + mcu_pwr = self.mcu.with_mixin(IoControllerPowerOut()) + mcu_usb = self.mcu.with_mixin(IoControllerUsbOut()) + + self.motor_pwr = self.Block(LipoConnector(voltage=(2.5, 4.2) * Volt * 6, actual_voltage=(2.5, 4.2) * Volt * 6)) + + self.vusb = self.connect(mcu_usb.vusb_out) + self.v3v3 = self.connect(mcu_pwr.pwr_out) + self.gnd = self.connect(self.mcu.gnd, self.motor_pwr.gnd) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.sw1,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw1")) + (self.ledr,), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request("ledr")) + (self.ledg,), _ = self.chain(imp.Block(IndicatorLed(Led.Green)), self.mcu.gpio.request("ledg")) + (self.ledb,), _ = self.chain(imp.Block(IndicatorLed(Led.Blue)), self.mcu.gpio.request("ledb")) + + i2c_bus = self.mcu.i2c.request("i2c") + (self.i2c_pull, self.i2c_tp, self.i2c), _ = self.chain( + i2c_bus, imp.Block(I2cPullup()), imp.Block(I2cTestPoint()), imp.Block(I2cConnector()) + ) + + (self.ref_div, self.ref_buf, self.ref_tp), _ = self.chain( + self.v3v3, + imp.Block(VoltageDivider(output_voltage=1.5 * Volt(tol=0.05), impedance=(10, 100) * kOhm)), + imp.Block(OpampFollower()), + self.Block(AnalogTestPoint()), + ) + self.vref = self.ref_buf.output + + # HALL SENSOR + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.hall = imp.Block(BldcHallSensor()) + self.connect(self.vusb, self.hall.pwr) + + (self.hall_pull, self.hall_tp), _ = self.chain( + self.hall.phases, + self.Block(PullupResistorArray(4.7 * kOhm(tol=0.05))), + self.Block(DigitalArrayTestPoint()), + self.mcu.gpio.request_vector("hall"), + ) + self.connect(self.hall_pull.pwr, self.v3v3) + + # BLDC CONTROLLER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.vsense = imp.Block( + VoltageSenseDivider(full_scale_voltage=(3.0, 3.3) * Volt, impedance=10 * kOhm(tol=0.2)) + ) + self.connect(self.motor_pwr.pwr, self.vsense.input) + (self.vsense_tp,), _ = self.chain( + self.vsense.output, self.Block(AnalogTestPoint()), self.mcu.adc.request("vsense") + ) + + self.isense = imp.Block( + OpampCurrentSensor( + resistance=0.05 * Ohm(tol=0.01), + ratio=Range.from_tolerance(10, 0.05), + input_impedance=10 * kOhm(tol=0.05), + ) + ) + self.connect(self.motor_pwr.pwr, self.isense.pwr_in, self.isense.pwr) + self.connect(self.isense.ref, self.vref) + (self.isense_tp, self.isense_clamp), _ = self.chain( + self.isense.out, + self.Block(AnalogTestPoint()), + imp.Block(AnalogClampResistor()), + self.mcu.adc.request("isense"), + ) + + self.bldc_drv = imp.Block(Drv8313(risense_res=50 * mOhm(tol=0.05))) + self.connect(self.isense.pwr_out, self.bldc_drv.pwr) + + self.connect(self.mcu.gpio.request("bldc_reset"), self.bldc_drv.nreset) + (self.bldc_fault_tp,), _ = self.chain( + self.mcu.gpio.request("bldc_fault"), self.Block(DigitalTestPoint()), self.bldc_drv.nfault + ) + (self.bldc_en_tp,), _ = self.chain( + self.mcu.gpio.request_vector("bldc_en"), self.Block(DigitalArrayTestPoint()), self.bldc_drv.ens + ) + (self.bldc_in_tp,), _ = self.chain( + self.mcu.gpio.request_vector("bldc_in"), self.Block(DigitalArrayTestPoint()), self.bldc_drv.ins + ) + + self.bldc = imp.Block(BldcConnector(2.5 * Amp)) # maximum of DRV8313 + self.connect(self.bldc_drv.outs.request_vector(), self.bldc.phases) + + self.curr = ElementDict[CurrentSenseResistor]() + self.curr_amp = ElementDict[Amplifier]() + self.curr_tp = ElementDict[AnalogTestPoint]() + for i in ["1", "2", "3"]: + self.curr_amp[i] = imp.Block(Amplifier(Range.from_tolerance(20, 0.05))) + self.connect(self.curr_amp[i].pwr, self.v3v3) + ( + _, + self.curr_tp[i], + ), _ = self.chain( + self.bldc_drv.pgnd_sense.request(i), + self.curr_amp[i], + self.Block(AnalogTestPoint()), + self.mcu.adc.request(f"curr_{i}"), + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[(["mcu"], Feather_Nrf52840), (["isense", "amp", "amp"], Opa197)], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "ledb=3", + "isense=5", + "vsense=6", + "ledg=7", + "curr_3=8", + "curr_2=9", + "curr_1=10", + "bldc_in_1=11", + "bldc_en_1=12", + "bldc_in_2=13", + "bldc_en_2=14", + "bldc_in_3=15", + "ledr=16", + "bldc_en_3=17", + "bldc_reset=18", + "bldc_fault=19", + "sw1=20", + "i2c.sda=21", + "i2c.scl=22", + "hall_u=23", + "hall_v=24", + "hall_w=25", + ], + ), + (["isense", "sense", "res", "res", "require_basic_part"], False), + (["bldc_drv", "pgnd_res[1]", "res", "res", "require_basic_part"], False), + (["bldc_drv", "pgnd_res[1]", "res", "res", "footprint_spec"], "Resistor_SMD:R_2512_6332Metric"), + ( + ["bldc_drv", "pgnd_res[2]", "res", "res", "require_basic_part"], + ParamValue(["bldc_drv", "pgnd_res[1]", "res", "res", "require_basic_part"]), + ), + ( + ["bldc_drv", "pgnd_res[2]", "res", "res", "footprint_spec"], + ParamValue(["bldc_drv", "pgnd_res[1]", "res", "res", "footprint_spec"]), + ), + ( + ["bldc_drv", "pgnd_res[3]", "res", "res", "require_basic_part"], + ParamValue(["bldc_drv", "pgnd_res[1]", "res", "res", "require_basic_part"]), + ), + ( + ["bldc_drv", "pgnd_res[3]", "res", "res", "footprint_spec"], + ParamValue(["bldc_drv", "pgnd_res[1]", "res", "res", "footprint_spec"]), + ), + (["bldc_drv", "vm_cap_bulk", "cap", "voltage_rating_derating"], 0.6), # allow using a 50V cap + (["bldc_drv", "cp_cap", "voltage_rating_derating"], 0.6), # allow using a 50V cap + (["hall", "pwr", "voltage_limits"], Range(4, 5.5)), # allow with the Feather Vbus diode drop + ], + class_refinements=[ + (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified + (TestPoint, CompactKeystone5015), + ], + class_values=[], + ) class BldcControllerTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(BldcController) + def test_design(self) -> None: + compile_board_inplace(BldcController) diff --git a/examples/test_ble_joystick.py b/examples/test_ble_joystick.py index 1f19e47d3..156d0fcd4 100644 --- a/examples/test_ble_joystick.py +++ b/examples/test_ble_joystick.py @@ -6,17 +6,16 @@ class BleJoystick(JlcBoardTop): - """BLE joystick with XYAB buttons - """ + """BLE joystick with XYAB buttons""" + @override def contents(self) -> None: super().contents() # really should operate down to ~3.3v, # this forces the model to allow the LDO to go into tracking - self.bat = self.Block(LipoConnector(voltage=(4.0, 4.2)*Volt, - actual_voltage=(4.0, 4.2)*Volt)) - self.usb = self.Block(UsbCReceptacle(current_limits=(0, 1)*Amp)) + self.bat = self.Block(LipoConnector(voltage=(4.0, 4.2) * Volt, actual_voltage=(4.0, 4.2) * Volt)) + self.usb = self.Block(UsbCReceptacle(current_limits=(0, 1) * Amp)) self.vbat = self.connect(self.bat.pwr) self.vusb = self.connect(self.usb.pwr) @@ -28,7 +27,7 @@ def contents(self) -> None: # POWER with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.gnd, [Common]), ) as imp: # (self.gate, self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( # self.vbat, @@ -42,20 +41,19 @@ def contents(self) -> None: # self.connect(self.usb.pwr, self.chg.pwr) # self.connect(self.chg.pwr_bat, self.bat.chg) - self.mp2722 = imp.Block(Mp2722(output_voltage=(3.7, 4.35)*Volt, - charging_current=200*mAmp(tol=0.2))) + self.mp2722 = imp.Block(Mp2722(output_voltage=(3.7, 4.35) * Volt, charging_current=200 * mAmp(tol=0.2))) self.connect(self.mp2722.pwr_in, self.usb.pwr) self.connect(self.mp2722.batt, self.vbat) self.connect(self.mp2722.cc, self.usb.cc) (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( self.mp2722.pwr_out, - imp.Block(VoltageRegulator(output_voltage=3.3*Volt(tol=0.05))), + imp.Block(VoltageRegulator(output_voltage=3.3 * Volt(tol=0.05))), self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), ) self.v3v3 = self.connect(self.reg_3v3.pwr_out) - self.fake_ntc = imp.Block(VoltageDivider(output_voltage=(1.5, 2)*Volt, impedance=(10, 100)*kOhm)) + self.fake_ntc = imp.Block(VoltageDivider(output_voltage=(1.5, 2) * Volt, impedance=(10, 100) * kOhm)) self.connect(self.mp2722.vrntc, self.fake_ntc.input) self.connect(self.fake_ntc.output, self.mp2722.ntc1) # TODO actual NTC @@ -64,85 +62,90 @@ def contents(self) -> None: # 3V3 DOMAIN with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: self.mcu = imp.Block(IoController()) self.mcu.with_mixin(IoControllerWifi()) self.stick = imp.Block(XboxElite2Joystick()) - (self.ax1_div, ), _ = self.chain(self.stick.ax1, - imp.Block(SignalDivider(ratio=(0.45, 0.55), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('ax1')) - (self.ax2_div, ), _ = self.chain(self.stick.ax2, - imp.Block(SignalDivider(ratio=(0.45, 0.55), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('ax2')) + (self.ax1_div,), _ = self.chain( + self.stick.ax1, + imp.Block(SignalDivider(ratio=(0.45, 0.55), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("ax1"), + ) + (self.ax2_div,), _ = self.chain( + self.stick.ax2, + imp.Block(SignalDivider(ratio=(0.45, 0.55), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("ax2"), + ) # self.connect(self.stick.sw, self.gate.btn_in) # self.connect(self.gate.btn_out, self.mcu.gpio.request('sw')) # self.connect(self.mcu.gpio.request('gate_ctl'), self.gate.control) - self.connect(self.stick.sw, self.mcu.gpio.request('sw'), self.mp2722.rst) + self.connect(self.stick.sw, self.mcu.gpio.request("sw"), self.mp2722.rst) self.trig = imp.Block(A1304()) - (self.trig_div, ), _ = self.chain(self.trig.out, - imp.Block(SignalDivider(ratio=(0.45, 0.55), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('trig')) + (self.trig_div,), _ = self.chain( + self.trig.out, + imp.Block(SignalDivider(ratio=(0.45, 0.55), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("trig"), + ) self.sw = ElementDict[DigitalSwitch]() for i in range(3): sw = self.sw[i] = imp.Block(DigitalSwitch()) - self.connect(sw.out, self.mcu.gpio.request(f'sw{i}')) + self.connect(sw.out, self.mcu.gpio.request(f"sw{i}")) # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request('led')) + (self.ledr,), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request("led")) # self.connect(self.vbat_sense_gate.control, self.mcu.gpio.request('vbat_sense_gate')) - (self.vbat_sense, ), _ = self.chain( + (self.vbat_sense,), _ = self.chain( # self.vbat_sense_gate.output, self.vbat, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('vbat_sense') + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("vbat_sense"), ) - (self.i2c_pull, ), _ = self.chain( - self.mcu.i2c.request('i2c'), - imp.Block(I2cPullup()), - self.mp2722.i2c - ) + (self.i2c_pull,), _ = self.chain(self.mcu.i2c.request("i2c"), imp.Block(I2cPullup()), self.mp2722.i2c) @override def refinements(self) -> Refinements: return super().refinements() + Refinements( instance_refinements=[ - (['mcu'], Esp32c3_Wroom02), - (['reg_3v3'], Ap7215), - (['sw[0]', 'package'], SmtSwitch), - (['sw[1]', 'package'], SmtSwitch), - (['sw[2]', 'package'], SmtSwitch), - (['mcu', 'boot', 'package'], SmtSwitch), + (["mcu"], Esp32c3_Wroom02), + (["reg_3v3"], Ap7215), + (["sw[0]", "package"], SmtSwitch), + (["sw[1]", "package"], SmtSwitch), + (["sw[2]", "package"], SmtSwitch), + (["mcu", "boot", "package"], SmtSwitch), ], instance_values=[ - (['refdes_prefix'], 'J'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'led=_GPIO9_STRAP', # force using the strapping / boot mode pin - # note, only ADC pins are IO0/1/3/4/5 (pins 18/17/15/3/4) - 'ax1=3', - 'ax2=15', - 'trig=17', - 'vbat_sense=18', - # 'vbat_sense_gate=14', - # 'gate_ctl=5', - 'i2c.scl=4', - 'i2c.sda=14', - 'sw=5', # joystick - 'sw0=10', # membranes - 'sw1=13', - 'sw2=6', - ]), - (['mcu', 'programming'], 'uart-auto-button'), - (['mp2722', 'power_path', 'dutycycle_limit'], Range(0.1, 1)), # allow tracking - (['mp2722', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0.0, 10e6)), - (['mp2722', 'power_path', 'inductor', 'footprint_spec'], "Inductor_SMD:L_1210_3225Metric") + (["refdes_prefix"], "J"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "led=_GPIO9_STRAP", # force using the strapping / boot mode pin + # note, only ADC pins are IO0/1/3/4/5 (pins 18/17/15/3/4) + "ax1=3", + "ax2=15", + "trig=17", + "vbat_sense=18", + # 'vbat_sense_gate=14', + # 'gate_ctl=5', + "i2c.scl=4", + "i2c.sda=14", + "sw=5", # joystick + "sw0=10", # membranes + "sw1=13", + "sw2=6", + ], + ), + (["mcu", "programming"], "uart-auto-button"), + (["mp2722", "power_path", "dutycycle_limit"], Range(0.1, 1)), # allow tracking + (["mp2722", "power_path", "inductor", "manual_frequency_rating"], Range(0.0, 10e6)), + (["mp2722", "power_path", "inductor", "footprint_spec"], "Inductor_SMD:L_1210_3225Metric"), ], class_refinements=[ (EspProgrammingHeader, EspProgrammingTc2030), @@ -150,9 +153,9 @@ def refinements(self) -> Refinements: (PassiveConnector, JstPhKVertical), ], class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), - (SmtSwitch, ['fp_footprint'], "project:MembraneSwitch_4mm") - ] + (CompactKeystone5015, ["lcsc_part"], "C5199798"), + (SmtSwitch, ["fp_footprint"], "project:MembraneSwitch_4mm"), + ], ) diff --git a/examples/test_blinky.py b/examples/test_blinky.py index 0b2ffa323..1017edd84 100644 --- a/examples/test_blinky.py +++ b/examples/test_blinky.py @@ -6,639 +6,670 @@ class TestLed(SimpleBoardTop): - """The actually simplest circuit, a LED connected to a dummy source.""" - @override - def contents(self) -> None: - self.gnd = self.Block(DummyGround()) - self.src = self.Block(DummyDigitalSource()) - self.led = self.Block(IndicatorLed()) + """The actually simplest circuit, a LED connected to a dummy source.""" - self.connect(self.led.signal, self.src.io) - self.connect(self.gnd.gnd, self.led.gnd) + @override + def contents(self) -> None: + self.gnd = self.Block(DummyGround()) + self.src = self.Block(DummyDigitalSource()) + self.led = self.Block(IndicatorLed()) + + self.connect(self.led.signal, self.src.io) + self.connect(self.gnd.gnd, self.led.gnd) class TestBlinkyBasic(SimpleBoardTop): - """The simplest cirucit, a microcontroller dev board with a LED.""" - @override - def contents(self) -> None: - self.mcu = self.Block(Nucleo_F303k8()) - self.led = self.Block(IndicatorLed()) + """The simplest cirucit, a microcontroller dev board with a LED.""" + + @override + def contents(self) -> None: + self.mcu = self.Block(Nucleo_F303k8()) + self.led = self.Block(IndicatorLed()) - self.connect(self.led.signal, self.mcu.gpio.request()) - self.connect(self.mcu.gnd, self.led.gnd) + self.connect(self.led.signal, self.mcu.gpio.request()) + self.connect(self.mcu.gnd, self.led.gnd) class TestBlinkyEmpty(SimpleBoardTop): - pass + pass class TestBlinkyBasicBattery(SimpleBoardTop): - """The simplest cirucit, a microcontroller dev board with a LED, powered from a battery""" - @override - def contents(self) -> None: - self.bat = self.Block(AaBatteryStack(4)) - self.mcu = self.Block(Xiao_Rp2040()) - self.led = self.Block(IndicatorLed()) + """The simplest cirucit, a microcontroller dev board with a LED, powered from a battery""" + + @override + def contents(self) -> None: + self.bat = self.Block(AaBatteryStack(4)) + self.mcu = self.Block(Xiao_Rp2040()) + self.led = self.Block(IndicatorLed()) - self.connect(self.mcu.pwr_vin, self.bat.pwr) - self.connect(self.mcu.gnd, self.bat.gnd) - self.connect(self.led.signal, self.mcu.gpio.request()) - self.connect(self.mcu.gnd, self.led.gnd) + self.connect(self.mcu.pwr_vin, self.bat.pwr) + self.connect(self.mcu.gnd, self.bat.gnd) + self.connect(self.led.signal, self.mcu.gpio.request()) + self.connect(self.mcu.gnd, self.led.gnd) class TestBlinkyIncomplete(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.mcu = self.Block(Stm32f103_48()) - self.led = self.Block(IndicatorLed()) - self.connect(self.usb.gnd, self.mcu.gnd, self.led.gnd) - self.connect(self.usb.pwr, self.mcu.pwr) - self.connect(self.mcu.gpio.request('led'), self.led.signal) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.mcu = self.Block(Stm32f103_48()) + self.led = self.Block(IndicatorLed()) + self.connect(self.usb.gnd, self.mcu.gnd, self.led.gnd) + self.connect(self.usb.pwr, self.mcu.pwr) + self.connect(self.mcu.gpio.request("led"), self.led.signal) class TestBlinkyRegulated(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.mcu = self.Block(Stm32f103_48()) - self.led = self.Block(IndicatorLed()) - self.connect(self.usb.gnd, self.reg.gnd, self.mcu.gnd, self.led.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - self.connect(self.reg.pwr_out, self.mcu.pwr) - self.connect(self.mcu.gpio.request('led'), self.led.signal) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.mcu = self.Block(Stm32f103_48()) + self.led = self.Block(IndicatorLed()) + self.connect(self.usb.gnd, self.reg.gnd, self.mcu.gnd, self.led.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + self.connect(self.reg.pwr_out, self.mcu.pwr) + self.connect(self.mcu.gpio.request("led"), self.led.signal) class TestBlinkyComplete(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.mcu = self.Block(Stm32f103_48()) - self.led = self.Block(IndicatorLed()) - self.connect(self.usb.gnd, self.reg.gnd, self.mcu.gnd, self.led.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - self.connect(self.reg.pwr_out, self.mcu.pwr) - self.connect(self.mcu.gpio.request('led'), self.led.signal) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.mcu = self.Block(Stm32f103_48()) + self.led = self.Block(IndicatorLed()) + self.connect(self.usb.gnd, self.reg.gnd, self.mcu.gnd, self.led.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + self.connect(self.reg.pwr_out, self.mcu.pwr) + self.connect(self.mcu.gpio.request("led"), self.led.signal) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + ] + ) class TestBlinkyExpanded(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.mcu = self.Block(Stm32f103_48()) - self.connect(self.usb.gnd, self.reg.gnd, self.mcu.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - self.connect(self.reg.pwr_out, self.mcu.pwr) - - self.sw = self.Block(DigitalSwitch()) - self.connect(self.mcu.gpio.request('sw'), self.sw.out) - self.connect(self.usb.gnd, self.sw.gnd) - - self.led = ElementDict[IndicatorLed]() - for i in range(4): - self.led[i] = self.Block(IndicatorLed()) - self.connect(self.mcu.gpio.request(f'led{i}'), self.led[i].signal) - self.connect(self.usb.gnd, self.led[i].gnd) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.mcu = self.Block(Stm32f103_48()) + self.connect(self.usb.gnd, self.reg.gnd, self.mcu.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + self.connect(self.reg.pwr_out, self.mcu.pwr) + + self.sw = self.Block(DigitalSwitch()) + self.connect(self.mcu.gpio.request("sw"), self.sw.out) + self.connect(self.usb.gnd, self.sw.gnd) + + self.led = ElementDict[IndicatorLed]() + for i in range(4): + self.led[i] = self.Block(IndicatorLed()) + self.connect(self.mcu.gpio.request(f"led{i}"), self.led[i].signal) + self.connect(self.usb.gnd, self.led[i].gnd) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + ] + ) class TestBlinkyImplicit(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( - ImplicitConnect(self.reg.pwr_out, [Power]), - ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(Stm32f103_48()) - - self.sw = imp.Block(DigitalSwitch()) - self.connect(self.mcu.gpio.request('sw'), self.sw.out) - - self.led = ElementDict[IndicatorLed]() - for i in range(4): - self.led[i] = imp.Block(IndicatorLed()) - self.connect(self.mcu.gpio.request(f'led{i}'), self.led[i].signal) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( + ImplicitConnect(self.reg.pwr_out, [Power]), + ImplicitConnect(self.reg.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(Stm32f103_48()) + + self.sw = imp.Block(DigitalSwitch()) + self.connect(self.mcu.gpio.request("sw"), self.sw.out) + + self.led = ElementDict[IndicatorLed]() + for i in range(4): + self.led[i] = imp.Block(IndicatorLed()) + self.connect(self.mcu.gpio.request(f"led{i}"), self.led[i].signal) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + ] + ) class TestBlinkyChain(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( - ImplicitConnect(self.reg.pwr_out, [Power]), - ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(Stm32f103_48()) - - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) - - self.led = ElementDict[IndicatorLed]() - for i in range(4): - (self.led[i], ), _ = self.chain(self.mcu.gpio.request(f'led{i}'), imp.Block(IndicatorLed())) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( + ImplicitConnect(self.reg.pwr_out, [Power]), + ImplicitConnect(self.reg.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(Stm32f103_48()) + + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) + + self.led = ElementDict[IndicatorLed]() + for i in range(4): + (self.led[i],), _ = self.chain(self.mcu.gpio.request(f"led{i}"), imp.Block(IndicatorLed())) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + ] + ) class TestBlinkyMicro(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( - ImplicitConnect(self.reg.pwr_out, [Power]), - ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) - - self.led = ElementDict[IndicatorLed]() - for i in range(4): - (self.led[i], ), _ = self.chain(self.mcu.gpio.request(f'led{i}'), imp.Block(IndicatorLed())) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - (['mcu'], Esp32_Wroom_32), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'led0=26', - 'led1=27', - 'led2=28', - 'led3=29', - ]) - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( + ImplicitConnect(self.reg.pwr_out, [Power]), + ImplicitConnect(self.reg.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) + + self.led = ElementDict[IndicatorLed]() + for i in range(4): + (self.led[i],), _ = self.chain(self.mcu.gpio.request(f"led{i}"), imp.Block(IndicatorLed())) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + (["mcu"], Esp32_Wroom_32), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "led0=26", + "led1=27", + "led2=28", + "led3=29", + ], + ) + ], + ) class Lf21215tmr_Device(FootprintBlock): - def __init__(self) -> None: - super().__init__() - self.vcc = self.Port( - VoltageSink(voltage_limits=(1.8, 5.5)*Volt, current_draw=(0.5, 2.0)*uAmp)) - self.gnd = self.Port(Ground()) - - self.vout = self.Port(DigitalSource.from_supply( - self.gnd, self.vcc, - current_limits=(-9, 9)*mAmp, - output_threshold_offset=(0.2, -0.3) - )) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'Package_TO_SOT_SMD:SOT-23', - { - '1': self.vcc, - '2': self.vout, - '3': self.gnd, - }, - mfr='Littelfuse', part='LF21215TMR', - datasheet='https://www.littelfuse.com/~/media/electronics/datasheets/magnetic_sensors_and_reed_switches/littelfuse_tmr_switch_lf21215tmr_datasheet.pdf.pdf' - ) + def __init__(self) -> None: + super().__init__() + self.vcc = self.Port(VoltageSink(voltage_limits=(1.8, 5.5) * Volt, current_draw=(0.5, 2.0) * uAmp)) + self.gnd = self.Port(Ground()) + + self.vout = self.Port( + DigitalSource.from_supply( + self.gnd, self.vcc, current_limits=(-9, 9) * mAmp, output_threshold_offset=(0.2, -0.3) + ) + ) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "Package_TO_SOT_SMD:SOT-23", + { + "1": self.vcc, + "2": self.vout, + "3": self.gnd, + }, + mfr="Littelfuse", + part="LF21215TMR", + datasheet="https://www.littelfuse.com/~/media/electronics/datasheets/magnetic_sensors_and_reed_switches/littelfuse_tmr_switch_lf21215tmr_datasheet.pdf.pdf", + ) class Lf21215tmr(Block): - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Lf21215tmr_Device()) + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Lf21215tmr_Device()) - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - self.out = self.Port(DigitalSource.empty()) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + self.out = self.Port(DigitalSource.empty()) - self.cap = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2))) + self.cap = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))) - self.connect(self.ic.vcc, self.cap.pwr, self.pwr) - self.connect(self.ic.gnd, self.cap.gnd, self.gnd) - self.connect(self.ic.vout, self.out) + self.connect(self.ic.vcc, self.cap.pwr, self.pwr) + self.connect(self.ic.gnd, self.cap.gnd, self.gnd) + self.connect(self.ic.vout, self.out) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() class Lf21215tmr_Export(Block): - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Lf21215tmr_Device()) - self.pwr = self.Export(self.ic.vcc, [Power]) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.out = self.Export(self.ic.vout) + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Lf21215tmr_Device()) + self.pwr = self.Export(self.ic.vcc, [Power]) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.out = self.Export(self.ic.vout) - self.cap = self.Block(DecouplingCapacitor(capacitance=0.1*uFarad(tol=0.2))) - self.connect(self.cap.pwr, self.pwr) - self.connect(self.cap.gnd, self.gnd) + self.cap = self.Block(DecouplingCapacitor(capacitance=0.1 * uFarad(tol=0.2))) + self.connect(self.cap.pwr, self.pwr) + self.connect(self.cap.gnd, self.gnd) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() class TestBlinkyWithLibrary(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( - ImplicitConnect(self.reg.pwr_out, [Power]), - ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) - - self.led = ElementDict[IndicatorLed]() - for i in range(4): - (self.led[i], ), _ = self.chain(self.mcu.gpio.request(f'led{i}'), imp.Block(IndicatorLed())) - - self.mag = imp.Block(Lf21215tmr()) - self.connect(self.mcu.gpio.request('mag'), self.mag.out) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - (['mcu'], Esp32_Wroom_32), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'led0=26', - 'led1=27', - 'led2=28', - 'led3=29', - ]) - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( + ImplicitConnect(self.reg.pwr_out, [Power]), + ImplicitConnect(self.reg.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) + + self.led = ElementDict[IndicatorLed]() + for i in range(4): + (self.led[i],), _ = self.chain(self.mcu.gpio.request(f"led{i}"), imp.Block(IndicatorLed())) + + self.mag = imp.Block(Lf21215tmr()) + self.connect(self.mcu.gpio.request("mag"), self.mag.out) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + (["mcu"], Esp32_Wroom_32), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "led0=26", + "led1=27", + "led2=28", + "led3=29", + ], + ) + ], + ) class TestBlinkyWithLibraryExport(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( - ImplicitConnect(self.reg.pwr_out, [Power]), - ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) - - self.led = ElementDict[IndicatorLed]() - for i in range(4): - (self.led[i], ), _ = self.chain(self.mcu.gpio.request(f'led{i}'), imp.Block(IndicatorLed())) - - self.mag = imp.Block(Lf21215tmr_Export()) - self.connect(self.mcu.gpio.request('mag'), self.mag.out) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - (['mcu'], Esp32_Wroom_32), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'led0=26', - 'led1=27', - 'led2=28', - 'led3=29', - ]) - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( + ImplicitConnect(self.reg.pwr_out, [Power]), + ImplicitConnect(self.reg.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) + + self.led = ElementDict[IndicatorLed]() + for i in range(4): + (self.led[i],), _ = self.chain(self.mcu.gpio.request(f"led{i}"), imp.Block(IndicatorLed())) + + self.mag = imp.Block(Lf21215tmr_Export()) + self.connect(self.mcu.gpio.request("mag"), self.mag.out) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + (["mcu"], Esp32_Wroom_32), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "led0=26", + "led1=27", + "led2=28", + "led3=29", + ], + ) + ], + ) class LedArray(GeneratorBlock): - def __init__(self, count: IntLike) -> None: - super().__init__() - self.ios = self.Port(Vector(DigitalSink.empty()), [Input]) - self.gnd = self.Port(Ground.empty(), [Common]) - self.count = self.ArgParameter(count) - self.generator_param(self.count) - - @override - def generate(self) -> None: - super().generate() - self.led = ElementDict[IndicatorLed]() - for i in range(self.get(self.count)): - io = self.ios.append_elt(DigitalSink.empty()) - self.led[i] = self.Block(IndicatorLed()) - self.connect(io, self.led[i].signal) - self.connect(self.gnd, self.led[i].gnd) + def __init__(self, count: IntLike) -> None: + super().__init__() + self.ios = self.Port(Vector(DigitalSink.empty()), [Input]) + self.gnd = self.Port(Ground.empty(), [Common]) + self.count = self.ArgParameter(count) + self.generator_param(self.count) + + @override + def generate(self) -> None: + super().generate() + self.led = ElementDict[IndicatorLed]() + for i in range(self.get(self.count)): + io = self.ios.append_elt(DigitalSink.empty()) + self.led[i] = self.Block(IndicatorLed()) + self.connect(io, self.led[i].signal) + self.connect(self.gnd, self.led[i].gnd) class TestBlinkyArray(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( - ImplicitConnect(self.reg.pwr_out, [Power]), - ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) - - (self.led, ), _ = self.chain(self.mcu.gpio.request_vector('led'), imp.Block(LedArray(4))) - - # optionally, you may have also instantiated your magnetic sensor - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - (['mcu'], Esp32_Wroom_32), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'led_0=26', - 'led_1=27', - 'led_2=28', - 'led_3=29', - ]) - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( + ImplicitConnect(self.reg.pwr_out, [Power]), + ImplicitConnect(self.reg.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) + + (self.led,), _ = self.chain(self.mcu.gpio.request_vector("led"), imp.Block(LedArray(4))) + + # optionally, you may have also instantiated your magnetic sensor + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + (["mcu"], Esp32_Wroom_32), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "led_0=26", + "led_1=27", + "led_2=28", + "led_3=29", + ], + ) + ], + ) class TestBlinkyPacked(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( - ImplicitConnect(self.reg.pwr_out, [Power]), - ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) - - (self.led, ), _ = self.chain(self.mcu.gpio.request_vector('led'), imp.Block(LedArray(4))) - - # optionally, you may have also instantiated your magnetic sensor - - @override - def multipack(self) -> None: - self.res_pack = self.PackedBlock(ResistorArray()) - self.pack(self.res_pack.elements.request('0'), ['led', 'led[0]', 'res']) - self.pack(self.res_pack.elements.request('1'), ['led', 'led[1]', 'res']) - self.pack(self.res_pack.elements.request('2'), ['led', 'led[2]', 'res']) - self.pack(self.res_pack.elements.request('3'), ['led', 'led[3]', 'res']) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - (['mcu'], Esp32_Wroom_32), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'led_0=26', - 'led_1=27', - 'led_2=28', - 'led_3=29', - ]) - ]) + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( + ImplicitConnect(self.reg.pwr_out, [Power]), + ImplicitConnect(self.reg.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) + + (self.led,), _ = self.chain(self.mcu.gpio.request_vector("led"), imp.Block(LedArray(4))) + + # optionally, you may have also instantiated your magnetic sensor + + @override + def multipack(self) -> None: + self.res_pack = self.PackedBlock(ResistorArray()) + self.pack(self.res_pack.elements.request("0"), ["led", "led[0]", "res"]) + self.pack(self.res_pack.elements.request("1"), ["led", "led[1]", "res"]) + self.pack(self.res_pack.elements.request("2"), ["led", "led[2]", "res"]) + self.pack(self.res_pack.elements.request("3"), ["led", "led[3]", "res"]) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + (["mcu"], Esp32_Wroom_32), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "led_0=26", + "led_1=27", + "led_2=28", + "led_3=29", + ], + ) + ], + ) class Hx711(KiCadSchematicBlock): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.dout = self.Port(DigitalSource.empty()) - self.sck = self.Port(DigitalSink.empty()) + self.dout = self.Port(DigitalSource.empty()) + self.sck = self.Port(DigitalSink.empty()) - self.ep = self.Port(Passive.empty()) - self.en = self.Port(Passive.empty()) - self.sp = self.Port(Passive.empty()) - self.sn = self.Port(Passive.empty()) + self.ep = self.Port(Passive.empty()) + self.en = self.Port(Passive.empty()) + self.sp = self.Port(Passive.empty()) + self.sn = self.Port(Passive.empty()) - @override - def contents(self) -> None: - super().contents() - self.Q1 = self.Block(Bjt.Npn((0, 5)*Volt, 0*Amp(tol=0))) - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - auto_adapt=True) + @override + def contents(self) -> None: + super().contents() + self.Q1 = self.Block(Bjt.Npn((0, 5) * Volt, 0 * Amp(tol=0))) + self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), auto_adapt=True) class TestBlinkyWithSchematicImport(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3*Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( ImplicitConnect(self.reg.pwr_out, [Power]), ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - self.conn = self.Block(PassiveConnector(4)) - self.sense = imp.Block(Hx711()) - self.connect(self.mcu.gpio.request('hx711_dout'), self.sense.dout) - self.connect(self.mcu.gpio.request('hx711_sck'), self.sense.sck) - self.connect(self.conn.pins.request('1'), self.sense.ep) - self.connect(self.conn.pins.request('2'), self.sense.en) - self.connect(self.conn.pins.request('3'), self.sense.sp) - self.connect(self.conn.pins.request('4'), self.sense.sn) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - (['mcu'], Esp32_Wroom_32), - ]) + ) as imp: + self.mcu = imp.Block(IoController()) + + self.conn = self.Block(PassiveConnector(4)) + self.sense = imp.Block(Hx711()) + self.connect(self.mcu.gpio.request("hx711_dout"), self.sense.dout) + self.connect(self.mcu.gpio.request("hx711_sck"), self.sense.sck) + self.connect(self.conn.pins.request("1"), self.sense.ep) + self.connect(self.conn.pins.request("2"), self.sense.en) + self.connect(self.conn.pins.request("3"), self.sense.sp) + self.connect(self.conn.pins.request("4"), self.sense.sn) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + (["mcu"], Esp32_Wroom_32), + ] + ) class Hx711Modeled(KiCadSchematicBlock): - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.dout = self.Port(DigitalSource.empty()) - self.sck = self.Port(DigitalSink.empty()) - - self.ep = self.Port(Passive.empty()) - self.en = self.Port(Passive.empty()) - self.sp = self.Port(Passive.empty()) - self.sn = self.Port(Passive.empty()) - - @override - def contents(self) -> None: - super().contents() - self.Q1 = self.Block(Bjt.Npn((0, 5)*Volt, 0*Amp(tol=0))) - self.import_kicad(self.file_path("resources", "Hx711.kicad_sch"), - conversions={ - 'pwr': VoltageSink( - voltage_limits=(2.6, 5.5)*Volt, - current_draw=(0.3 + 0.2, 1400 + 100)*uAmp), # TODO: also model draw of external bridge? - 'gnd': Ground(), - 'dout': DigitalSource.from_supply(self.gnd, self.pwr), - 'sck': DigitalSink.from_supply(self.gnd, self.pwr), - }) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.dout = self.Port(DigitalSource.empty()) + self.sck = self.Port(DigitalSink.empty()) + + self.ep = self.Port(Passive.empty()) + self.en = self.Port(Passive.empty()) + self.sp = self.Port(Passive.empty()) + self.sn = self.Port(Passive.empty()) + + @override + def contents(self) -> None: + super().contents() + self.Q1 = self.Block(Bjt.Npn((0, 5) * Volt, 0 * Amp(tol=0))) + self.import_kicad( + self.file_path("resources", "Hx711.kicad_sch"), + conversions={ + "pwr": VoltageSink( + voltage_limits=(2.6, 5.5) * Volt, current_draw=(0.3 + 0.2, 1400 + 100) * uAmp + ), # TODO: also model draw of external bridge? + "gnd": Ground(), + "dout": DigitalSource.from_supply(self.gnd, self.pwr), + "sck": DigitalSink.from_supply(self.gnd, self.pwr), + }, + ) class TestBlinkyWithModeledSchematicImport(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(VoltageRegulator(3.3*Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) - - with self.implicit_connect( + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) + + with self.implicit_connect( ImplicitConnect(self.reg.pwr_out, [Power]), ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - self.conn = self.Block(PassiveConnector(4)) - self.sense = imp.Block(Hx711Modeled()) - self.connect(self.mcu.gpio.request('hx711_dout'), self.sense.dout) - self.connect(self.mcu.gpio.request('hx711_sck'), self.sense.sck) - self.connect(self.conn.pins.request('1'), self.sense.ep) - self.connect(self.conn.pins.request('2'), self.sense.en) - self.connect(self.conn.pins.request('3'), self.sense.sp) - self.connect(self.conn.pins.request('4'), self.sense.sn) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg'], Tps561201), - (['mcu'], Esp32_Wroom_32), - ]) + ) as imp: + self.mcu = imp.Block(IoController()) + + self.conn = self.Block(PassiveConnector(4)) + self.sense = imp.Block(Hx711Modeled()) + self.connect(self.mcu.gpio.request("hx711_dout"), self.sense.dout) + self.connect(self.mcu.gpio.request("hx711_sck"), self.sense.sck) + self.connect(self.conn.pins.request("1"), self.sense.ep) + self.connect(self.conn.pins.request("2"), self.sense.en) + self.connect(self.conn.pins.request("3"), self.sense.sp) + self.connect(self.conn.pins.request("4"), self.sense.sn) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg"], Tps561201), + (["mcu"], Esp32_Wroom_32), + ] + ) class BlinkyTestCase(unittest.TestCase): - def test_led(self) -> None: - compile_board_inplace(TestLed) + def test_led(self) -> None: + compile_board_inplace(TestLed) - def test_design_basic(self) -> None: - compile_board_inplace(TestBlinkyBasic) # generate this netlist as a test + def test_design_basic(self) -> None: + compile_board_inplace(TestBlinkyBasic) # generate this netlist as a test - def test_design_empty(self) -> None: - compile_board_inplace(TestBlinkyEmpty) + def test_design_empty(self) -> None: + compile_board_inplace(TestBlinkyEmpty) - def test_design_battery(self) -> None: - compile_board_inplace(TestBlinkyBasicBattery) + def test_design_battery(self) -> None: + compile_board_inplace(TestBlinkyBasicBattery) - def test_design_incomplete(self) -> None: - with self.assertRaises(CompilerCheckError): - compile_board_inplace(TestBlinkyIncomplete, False) + def test_design_incomplete(self) -> None: + with self.assertRaises(CompilerCheckError): + compile_board_inplace(TestBlinkyIncomplete, False) - def test_design_regulated(self) -> None: - with self.assertRaises(CompilerCheckError): - compile_board_inplace(TestBlinkyRegulated, False) + def test_design_regulated(self) -> None: + with self.assertRaises(CompilerCheckError): + compile_board_inplace(TestBlinkyRegulated, False) - def test_design_complete(self) -> None: - compile_board_inplace(TestBlinkyComplete) + def test_design_complete(self) -> None: + compile_board_inplace(TestBlinkyComplete) - def test_design_expnaded(self) -> None: - compile_board_inplace(TestBlinkyExpanded) + def test_design_expnaded(self) -> None: + compile_board_inplace(TestBlinkyExpanded) - def test_design_implicit(self) -> None: - compile_board_inplace(TestBlinkyImplicit) + def test_design_implicit(self) -> None: + compile_board_inplace(TestBlinkyImplicit) - def test_design_chain(self) -> None: - compile_board_inplace(TestBlinkyChain) # generate this netlist as a test + def test_design_chain(self) -> None: + compile_board_inplace(TestBlinkyChain) # generate this netlist as a test - def test_design_micro(self) -> None: - compile_board_inplace(TestBlinkyMicro) + def test_design_micro(self) -> None: + compile_board_inplace(TestBlinkyMicro) - def test_design_library(self) -> None: - compile_board_inplace(TestBlinkyWithLibrary) + def test_design_library(self) -> None: + compile_board_inplace(TestBlinkyWithLibrary) - def test_design_export(self) -> None: - compile_board_inplace(TestBlinkyWithLibraryExport) + def test_design_export(self) -> None: + compile_board_inplace(TestBlinkyWithLibraryExport) - def test_design_array(self) -> None: - compile_board_inplace(TestBlinkyArray) + def test_design_array(self) -> None: + compile_board_inplace(TestBlinkyArray) - def test_design_packed(self) -> None: - compile_board_inplace(TestBlinkyPacked) + def test_design_packed(self) -> None: + compile_board_inplace(TestBlinkyPacked) - def test_design_schematic_import(self) -> None: - compile_board_inplace(TestBlinkyWithSchematicImport) + def test_design_schematic_import(self) -> None: + compile_board_inplace(TestBlinkyWithSchematicImport) - def test_design_schematic_import_modeled(self) -> None: - compile_board_inplace(TestBlinkyWithModeledSchematicImport) + def test_design_schematic_import_modeled(self) -> None: + compile_board_inplace(TestBlinkyWithModeledSchematicImport) diff --git a/examples/test_can_adapter.py b/examples/test_can_adapter.py index 35b69970c..1c363c774 100644 --- a/examples/test_can_adapter.py +++ b/examples/test_can_adapter.py @@ -6,105 +6,109 @@ class Obd2Connector(FootprintBlock): - """OBD2 dongle-side (not car-side) connector""" - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground()) - self.pwr = self.Port(VoltageSource(voltage_out=(10, 25)*Volt)) - - self.can = self.Port(CanDiffPort()) - - @override - def contents(self) -> None: - super().contents() - self.footprint( - 'U', 'project:J1962', - { - '6': self.can.canh, - '14': self.can.canl, - '5': self.gnd, # note, 4 is chassis gnd - '16': self.pwr, # battery voltage - }, - ) + """OBD2 dongle-side (not car-side) connector""" + + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground()) + self.pwr = self.Port(VoltageSource(voltage_out=(10, 25) * Volt)) + + self.can = self.Port(CanDiffPort()) + + @override + def contents(self) -> None: + super().contents() + self.footprint( + "U", + "project:J1962", + { + "6": self.can.canh, + "14": self.can.canl, + "5": self.gnd, # note, 4 is chassis gnd + "16": self.pwr, # battery voltage + }, + ) class CanAdapter(JlcBoardTop): - @override - def contents(self) -> None: - super().contents() - - self.obd = self.Block(Obd2Connector()) - self.gnd = self.connect(self.obd.gnd) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.ferrite, self.reg_3v3, self.prot_3v3), _ = self.chain( - self.obd.pwr, - self.Block(SeriesPowerFerriteBead()), - imp.Block(VoltageRegulator(output_voltage=3.3*Volt(tol=0.05))), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.vobd = self.connect(self.ferrite.pwr_out) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerWifi()) - - self.can = imp.Block(CanTransceiver()) - self.connect(self.can.can, self.obd.can) - self.connect(self.can.controller, self.mcu.with_mixin(IoControllerCan()).can.request('can')) - - # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request('ledr')) - (self.ledg, ), _ = self.chain(imp.Block(IndicatorLed(Led.Green)), self.mcu.gpio.request('ledg')) - (self.ledw, ), _ = self.chain(imp.Block(IndicatorLed(Led.White)), self.mcu.gpio.request('ledw')) - - (self.vobd_sense, ), _ = self.chain( - self.vobd, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('vobd_sense') - ) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32c3_Wroom02), - (['reg_3v3'], Tps54202h), - ], - instance_values=[ - (['refdes_prefix'], 'O'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'ledr=_GPIO9_STRAP', # force using the strapping / boot mode pin - 'ledg=13', - 'ledw=14', - 'can.txd=6', - 'can.rxd=5', - 'vobd_sense=3', # 4 as sent to fabrication before ADC2 removed from model, blue-wire to 3 - - ]), - (['mcu', 'programming'], 'uart-auto'), - (['reg_3v3', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 9e6)), - (['reg_3v3', 'power_path', 'in_cap', 'cap', 'voltage_rating_derating'], 1.0), - (['reg_3v3', 'power_path', 'inductor', 'footprint_spec'], "Inductor_SMD:L_1210_3225Metric") - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (TagConnect, TagConnectNonLegged), - (CanTransceiver, Sn65hvd230), - (TestPoint, CompactKeystone5015), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), - ] - ) + @override + def contents(self) -> None: + super().contents() + + self.obd = self.Block(Obd2Connector()) + self.gnd = self.connect(self.obd.gnd) + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.ferrite, self.reg_3v3, self.prot_3v3), _ = self.chain( + self.obd.pwr, + self.Block(SeriesPowerFerriteBead()), + imp.Block(VoltageRegulator(output_voltage=3.3 * Volt(tol=0.05))), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.vobd = self.connect(self.ferrite.pwr_out) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerWifi()) + + self.can = imp.Block(CanTransceiver()) + self.connect(self.can.can, self.obd.can) + self.connect(self.can.controller, self.mcu.with_mixin(IoControllerCan()).can.request("can")) + + # debugging LEDs + (self.ledr,), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request("ledr")) + (self.ledg,), _ = self.chain(imp.Block(IndicatorLed(Led.Green)), self.mcu.gpio.request("ledg")) + (self.ledw,), _ = self.chain(imp.Block(IndicatorLed(Led.White)), self.mcu.gpio.request("ledw")) + + (self.vobd_sense,), _ = self.chain( + self.vobd, + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("vobd_sense"), + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32c3_Wroom02), + (["reg_3v3"], Tps54202h), + ], + instance_values=[ + (["refdes_prefix"], "O"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "ledr=_GPIO9_STRAP", # force using the strapping / boot mode pin + "ledg=13", + "ledw=14", + "can.txd=6", + "can.rxd=5", + "vobd_sense=3", # 4 as sent to fabrication before ADC2 removed from model, blue-wire to 3 + ], + ), + (["mcu", "programming"], "uart-auto"), + (["reg_3v3", "power_path", "inductor", "manual_frequency_rating"], Range(0, 9e6)), + (["reg_3v3", "power_path", "in_cap", "cap", "voltage_rating_derating"], 1.0), + (["reg_3v3", "power_path", "inductor", "footprint_spec"], "Inductor_SMD:L_1210_3225Metric"), + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (TagConnect, TagConnectNonLegged), + (CanTransceiver, Sn65hvd230), + (TestPoint, CompactKeystone5015), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), + ], + ) class CanAdapterTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(CanAdapter) + def test_design(self) -> None: + compile_board_inplace(CanAdapter) diff --git a/examples/test_datalogger.py b/examples/test_datalogger.py index e34e938e7..5b05b10f9 100644 --- a/examples/test_datalogger.py +++ b/examples/test_datalogger.py @@ -7,183 +7,189 @@ class Datalogger(BoardTop): - @override - def contents(self) -> None: - super().contents() - - self.pwr_conn = self.Block(CalSolPowerConnector()) - self.usb_conn = self.Block(UsbCReceptacle()) - - self.usb_forced_current = self.Block(ForcedVoltageCurrentDraw(forced_current_draw=(0, 0.5) * Amp)) - self.connect(self.usb_conn.pwr, self.usb_forced_current.pwr_in) - - self.bat = self.Block(Cr2032()) - self.gnd = self.connect(self.usb_conn.gnd, self.pwr_conn.gnd, self.bat.gnd) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.pwr_5v,), _ = self.chain( - self.pwr_conn.pwr, - imp.Block(BuckConverter(output_voltage=(4.85, 5.4)*Volt)) - ) - self.pwr_5v_merge = self.Block(MergedVoltageSource()).connected_from( - self.usb_forced_current.pwr_out, self.pwr_5v.pwr_out) - - (self.buffer, self.pwr_3v3), _ = self.chain( - self.pwr_5v_merge.pwr_out, - imp.Block(BufferedSupply(charging_current=(0.3, 0.4)*Amp, sense_resistance=0.47*Ohm(tol=0.01), - voltage_drop=(0, 0.4)*Volt)), - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))) - ) - - self.vin = self.connect(self.pwr_conn.pwr) - self.v5 = self.connect(self.pwr_5v_merge.pwr_out) - self.v5_buffered = self.connect(self.buffer.pwr_out) - self.v3v3 = self.connect(self.pwr_3v3.pwr_out) # TODO better auto net names - - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - # this uses the legacy / simple (non-mixin) USB and CAN IO style - self.connect(self.mcu.usb.request(), self.usb_conn.usb) - - (self.can, ), _ = self.chain(self.mcu.can.request('can'), imp.Block(CalSolCanBlock())) - - # mcu_i2c = self.mcu.i2c.request() # no devices, ignored for now - # self.i2c_pullup = imp.Block(I2cPullup()) - # self.connect(self.i2c_pullup.i2c, mcu_i2c) - - self.sd = imp.Block(SdSocket()) - self.connect(self.mcu.spi.request('sd_spi'), self.sd.spi) - self.connect(self.mcu.gpio.request('sd_cs'), self.sd.cs) - (self.cd_pull, ), _ = self.chain( - self.mcu.gpio.request('sd_cd_pull'), - imp.Block(PullupResistor(4.7 * kOhm(tol=0.05))), - self.sd.cd) - - self.xbee = imp.Block(Xbee_S3b()) - self.connect(self.mcu.uart.request('xbee_uart'), self.xbee.data) - (self.xbee_assoc, ), _ = self.chain( - self.xbee.associate, - imp.Block(IndicatorLed(current_draw=(0.5, 2)*mAmp))) # XBee DIO current is -2 -> 2 mA - - aux_spi = self.mcu.spi.request('aux_spi') - self.rtc = imp.Block(Pcf2129()) - self.connect(aux_spi, self.rtc.spi) - self.connect(self.mcu.gpio.request('rtc_cs'), self.rtc.cs) - self.connect(self.bat.pwr, self.rtc.pwr_bat) - - self.eink = imp.Block(E2154fs091()) - self.connect(aux_spi, self.eink.spi) - self.connect(self.mcu.gpio.request('eink_busy'), self.eink.busy) - self.connect(self.mcu.gpio.request('eink_reset'), self.eink.reset) - self.connect(self.mcu.gpio.request('eink_dc'), self.eink.dc) - self.connect(self.mcu.gpio.request('eink_cs'), self.eink.cs) - - self.ext = imp.Block(BlueSmirf()) - self.connect(self.mcu.uart.request('ext_uart'), self.ext.data) - self.connect(self.mcu.gpio.request('ext_cts'), self.ext.cts) - self.connect(self.mcu.gpio.request('ext_rts'), self.ext.rts) - - self.rgb1 = imp.Block(IndicatorSinkRgbLed()) # system RGB 1 - self.connect(self.mcu.gpio.request_vector('rgb1'), self.rgb1.signals) - - self.rgb2 = imp.Block(IndicatorSinkRgbLed()) # sd card RGB - self.connect(self.mcu.gpio.request_vector('rgb2'), self.rgb2.signals) - - self.rgb3 = imp.Block(IndicatorSinkRgbLed()) - self.connect(self.mcu.gpio.request_vector('rgb3'), self.rgb3.signals) - - sw_pull_model = PullupResistor(4.7 * kOhm(tol=0.05)) - (self.sw1, self.sw1_pull), _ = self.chain(imp.Block(DigitalSwitch()), - imp.Block(sw_pull_model), - self.mcu.gpio.request('sw1')) - (self.sw2, self.sw2_pull), _ = self.chain(imp.Block(DigitalSwitch()), - imp.Block(sw_pull_model), - self.mcu.gpio.request('sw2')) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - # TODO update to use VoltageSenseDivider - div_model = VoltageDivider(output_voltage=3 * Volt(tol=0.15), impedance=(100, 1000) * Ohm) - (self.v12sense, ), _ = self.chain(self.vin, imp.Block(div_model), self.mcu.adc.request('v12sense')) - (self.v5sense, ), _ = self.chain(self.v5, imp.Block(div_model), self.mcu.adc.request('v5sense')) - (self.vscsense, ), _ = self.chain(self.buffer.sc_out, imp.Block(div_model), self.mcu.adc.request('vscsense')) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Lpc1549_64), - (['pwr_5v'], Tps561201), - (['pwr_3v3'], Ldl1117), - (['buffer', 'amp'], Tlv9061), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'can.txd=51', - 'can.rxd=53', - 'sd_spi.sck=17', - 'sd_spi.mosi=15', - 'sd_spi.miso=19', - 'sd_cs=11', - 'sd_cd_pull=16', - 'xbee_uart.tx=58', - 'xbee_uart.rx=50', # used to be 54, which is ISP_0 - 'aux_spi.sck=5', - 'aux_spi.mosi=6', - 'aux_spi.miso=7', - 'rtc_cs=64', - 'eink_busy=1', - 'eink_reset=2', - 'eink_dc=3', - 'eink_cs=4', - 'eink_busy=1', - 'ext_uart.tx=60', - 'ext_uart.rx=61', - 'ext_cts=62', - 'ext_rts=59', - 'rgb1_red=31', - 'rgb1_green=32', - 'rgb1_blue=30', - 'rgb2_red=28', - 'rgb2_green=29', - 'rgb2_blue=25', - 'rgb3_red=46', - 'rgb3_green=39', - 'rgb3_blue=34', # used to be 38, which is ISP_1 - 'sw1=33', - 'sw2=23', - 'v12sense=10', - 'v5sense=9', - 'vscsense=8', - ]), - (['mcu', 'swd_swo_pin'], 'PIO0_8'), - - (['pwr_5v', 'power_path', 'inductor', 'part'], 'NR5040T220M'), # peg to prior part selection - (['pwr_5v', 'power_path', 'inductor_current_ripple'], Range(0.01, 0.6)), # trade higher Imax for lower L - # the hold current wasn't modeled at the time of manufacture and turns out to be out of limits - (['can', 'can_fuse', 'fuse', 'actual_hold_current'], Range(0.1, 0.1)), - # JLC does not have frequency specs, must be checked TODO - (['pwr_5v', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - (['eink', 'boost_ind', 'manual_frequency_rating'], Range.all()), - # JLC does not have gate voltage tolerance specs, and the inferred one is low - (['eink', 'boost_sw', 'gate_voltage'], Range(3, 10)), - - # keep netlist footprints as libraries change - (['buffer', 'fet', 'footprint_spec'], 'Package_TO_SOT_SMD:SOT-223-3_TabPin2'), - ], - class_refinements=[ - (Fuse, CanFuse) - ], - ) + @override + def contents(self) -> None: + super().contents() + + self.pwr_conn = self.Block(CalSolPowerConnector()) + self.usb_conn = self.Block(UsbCReceptacle()) + + self.usb_forced_current = self.Block(ForcedVoltageCurrentDraw(forced_current_draw=(0, 0.5) * Amp)) + self.connect(self.usb_conn.pwr, self.usb_forced_current.pwr_in) + + self.bat = self.Block(Cr2032()) + self.gnd = self.connect(self.usb_conn.gnd, self.pwr_conn.gnd, self.bat.gnd) + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.pwr_5v,), _ = self.chain( + self.pwr_conn.pwr, imp.Block(BuckConverter(output_voltage=(4.85, 5.4) * Volt)) + ) + self.pwr_5v_merge = self.Block(MergedVoltageSource()).connected_from( + self.usb_forced_current.pwr_out, self.pwr_5v.pwr_out + ) + + (self.buffer, self.pwr_3v3), _ = self.chain( + self.pwr_5v_merge.pwr_out, + imp.Block( + BufferedSupply( + charging_current=(0.3, 0.4) * Amp, + sense_resistance=0.47 * Ohm(tol=0.01), + voltage_drop=(0, 0.4) * Volt, + ) + ), + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + ) + + self.vin = self.connect(self.pwr_conn.pwr) + self.v5 = self.connect(self.pwr_5v_merge.pwr_out) + self.v5_buffered = self.connect(self.buffer.pwr_out) + self.v3v3 = self.connect(self.pwr_3v3.pwr_out) # TODO better auto net names + + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + # this uses the legacy / simple (non-mixin) USB and CAN IO style + self.connect(self.mcu.usb.request(), self.usb_conn.usb) + + (self.can,), _ = self.chain(self.mcu.can.request("can"), imp.Block(CalSolCanBlock())) + + # mcu_i2c = self.mcu.i2c.request() # no devices, ignored for now + # self.i2c_pullup = imp.Block(I2cPullup()) + # self.connect(self.i2c_pullup.i2c, mcu_i2c) + + self.sd = imp.Block(SdSocket()) + self.connect(self.mcu.spi.request("sd_spi"), self.sd.spi) + self.connect(self.mcu.gpio.request("sd_cs"), self.sd.cs) + (self.cd_pull,), _ = self.chain( + self.mcu.gpio.request("sd_cd_pull"), imp.Block(PullupResistor(4.7 * kOhm(tol=0.05))), self.sd.cd + ) + + self.xbee = imp.Block(Xbee_S3b()) + self.connect(self.mcu.uart.request("xbee_uart"), self.xbee.data) + (self.xbee_assoc,), _ = self.chain( + self.xbee.associate, imp.Block(IndicatorLed(current_draw=(0.5, 2) * mAmp)) + ) # XBee DIO current is -2 -> 2 mA + + aux_spi = self.mcu.spi.request("aux_spi") + self.rtc = imp.Block(Pcf2129()) + self.connect(aux_spi, self.rtc.spi) + self.connect(self.mcu.gpio.request("rtc_cs"), self.rtc.cs) + self.connect(self.bat.pwr, self.rtc.pwr_bat) + + self.eink = imp.Block(E2154fs091()) + self.connect(aux_spi, self.eink.spi) + self.connect(self.mcu.gpio.request("eink_busy"), self.eink.busy) + self.connect(self.mcu.gpio.request("eink_reset"), self.eink.reset) + self.connect(self.mcu.gpio.request("eink_dc"), self.eink.dc) + self.connect(self.mcu.gpio.request("eink_cs"), self.eink.cs) + + self.ext = imp.Block(BlueSmirf()) + self.connect(self.mcu.uart.request("ext_uart"), self.ext.data) + self.connect(self.mcu.gpio.request("ext_cts"), self.ext.cts) + self.connect(self.mcu.gpio.request("ext_rts"), self.ext.rts) + + self.rgb1 = imp.Block(IndicatorSinkRgbLed()) # system RGB 1 + self.connect(self.mcu.gpio.request_vector("rgb1"), self.rgb1.signals) + + self.rgb2 = imp.Block(IndicatorSinkRgbLed()) # sd card RGB + self.connect(self.mcu.gpio.request_vector("rgb2"), self.rgb2.signals) + + self.rgb3 = imp.Block(IndicatorSinkRgbLed()) + self.connect(self.mcu.gpio.request_vector("rgb3"), self.rgb3.signals) + + sw_pull_model = PullupResistor(4.7 * kOhm(tol=0.05)) + (self.sw1, self.sw1_pull), _ = self.chain( + imp.Block(DigitalSwitch()), imp.Block(sw_pull_model), self.mcu.gpio.request("sw1") + ) + (self.sw2, self.sw2_pull), _ = self.chain( + imp.Block(DigitalSwitch()), imp.Block(sw_pull_model), self.mcu.gpio.request("sw2") + ) + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + # TODO update to use VoltageSenseDivider + div_model = VoltageDivider(output_voltage=3 * Volt(tol=0.15), impedance=(100, 1000) * Ohm) + (self.v12sense,), _ = self.chain(self.vin, imp.Block(div_model), self.mcu.adc.request("v12sense")) + (self.v5sense,), _ = self.chain(self.v5, imp.Block(div_model), self.mcu.adc.request("v5sense")) + (self.vscsense,), _ = self.chain(self.buffer.sc_out, imp.Block(div_model), self.mcu.adc.request("vscsense")) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Lpc1549_64), + (["pwr_5v"], Tps561201), + (["pwr_3v3"], Ldl1117), + (["buffer", "amp"], Tlv9061), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "can.txd=51", + "can.rxd=53", + "sd_spi.sck=17", + "sd_spi.mosi=15", + "sd_spi.miso=19", + "sd_cs=11", + "sd_cd_pull=16", + "xbee_uart.tx=58", + "xbee_uart.rx=50", # used to be 54, which is ISP_0 + "aux_spi.sck=5", + "aux_spi.mosi=6", + "aux_spi.miso=7", + "rtc_cs=64", + "eink_busy=1", + "eink_reset=2", + "eink_dc=3", + "eink_cs=4", + "eink_busy=1", + "ext_uart.tx=60", + "ext_uart.rx=61", + "ext_cts=62", + "ext_rts=59", + "rgb1_red=31", + "rgb1_green=32", + "rgb1_blue=30", + "rgb2_red=28", + "rgb2_green=29", + "rgb2_blue=25", + "rgb3_red=46", + "rgb3_green=39", + "rgb3_blue=34", # used to be 38, which is ISP_1 + "sw1=33", + "sw2=23", + "v12sense=10", + "v5sense=9", + "vscsense=8", + ], + ), + (["mcu", "swd_swo_pin"], "PIO0_8"), + (["pwr_5v", "power_path", "inductor", "part"], "NR5040T220M"), # peg to prior part selection + ( + ["pwr_5v", "power_path", "inductor_current_ripple"], + Range(0.01, 0.6), + ), # trade higher Imax for lower L + # the hold current wasn't modeled at the time of manufacture and turns out to be out of limits + (["can", "can_fuse", "fuse", "actual_hold_current"], Range(0.1, 0.1)), + # JLC does not have frequency specs, must be checked TODO + (["pwr_5v", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + (["eink", "boost_ind", "manual_frequency_rating"], Range.all()), + # JLC does not have gate voltage tolerance specs, and the inferred one is low + (["eink", "boost_sw", "gate_voltage"], Range(3, 10)), + # keep netlist footprints as libraries change + (["buffer", "fet", "footprint_spec"], "Package_TO_SOT_SMD:SOT-223-3_TabPin2"), + ], + class_refinements=[(Fuse, CanFuse)], + ) class DataloggerTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(Datalogger) + def test_design(self) -> None: + compile_board_inplace(Datalogger) diff --git a/examples/test_deskcontroller.py b/examples/test_deskcontroller.py index 0e3b9d8ba..02121e5a9 100644 --- a/examples/test_deskcontroller.py +++ b/examples/test_deskcontroller.py @@ -8,25 +8,30 @@ class JiecangConnector(Block): """RJ-12 connector for (some?) Jiecang standing desk controllers https://github.com/phord/Jarvis?tab=readme-ov-file#physical-interface-rj-12""" + def __init__(self) -> None: super().__init__() self.conn = self.Block(PassiveConnector(length=6)) - self.gnd = self.Export(self.conn.pins.request('2').adapt_to(Ground()), [Common]) - self.pwr = self.Export(self.conn.pins.request('4').adapt_to(VoltageSource( - voltage_out=5*Volt(tol=0), - current_limits=(0, 300)*mAmp))) # reportedly drives at least 300mA + self.gnd = self.Export(self.conn.pins.request("2").adapt_to(Ground()), [Common]) + self.pwr = self.Export( + self.conn.pins.request("4").adapt_to( + VoltageSource(voltage_out=5 * Volt(tol=0), current_limits=(0, 300) * mAmp) + ) + ) # reportedly drives at least 300mA self.uart = self.Port(UartPort.empty()) # UART pins internally pulled up to 5v, need a level shifter self.pwr_io = self.Port(VoltageSink.empty()) - self.dtx_shift = self.Block(BidirectionaLevelShifter(hv_res=RangeExpr.INF, src_hint='hv')) - self.htx_shift = self.Block(BidirectionaLevelShifter(hv_res=RangeExpr.INF, src_hint='lv')) + self.dtx_shift = self.Block(BidirectionaLevelShifter(hv_res=RangeExpr.INF, src_hint="hv")) + self.htx_shift = self.Block(BidirectionaLevelShifter(hv_res=RangeExpr.INF, src_hint="lv")) self.connect(self.pwr, self.dtx_shift.hv_pwr, self.htx_shift.hv_pwr) self.connect(self.pwr_io, self.dtx_shift.lv_pwr, self.htx_shift.lv_pwr) - self.connect(self.dtx_shift.hv_io, self.conn.pins.request('3').adapt_to( - DigitalSource.from_supply(self.gnd, self.pwr))) # DTX, controller -> handset + self.connect( + self.dtx_shift.hv_io, self.conn.pins.request("3").adapt_to(DigitalSource.from_supply(self.gnd, self.pwr)) + ) # DTX, controller -> handset self.connect(self.dtx_shift.lv_io, self.uart.tx) - self.connect(self.htx_shift.hv_io, self.conn.pins.request('5').adapt_to( - DigitalSink.from_supply(self.gnd, self.pwr))) # HTX, handset -> controller + self.connect( + self.htx_shift.hv_io, self.conn.pins.request("5").adapt_to(DigitalSink.from_supply(self.gnd, self.pwr)) + ) # HTX, handset -> controller self.connect(self.htx_shift.lv_io, self.uart.rx) @@ -34,6 +39,7 @@ class DeskController(JlcBoardTop): """Standing desk controller for desks with a Jiecang controller https://community.home-assistant.io/t/desky-standing-desk-esphome-works-with-desky-uplift-jiecang-assmann-others/383790 """ + @override def contents(self) -> None: super().contents() @@ -43,102 +49,102 @@ def contents(self) -> None: self.tp_gnd = self.Block(GroundTestPoint()).connected(self.conn.gnd) with self.implicit_connect( # POWER - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.gnd, [Common]), ) as imp: (self.choke, self.tp_pwr), _ = self.chain( - self.conn.pwr, - self.Block(SeriesPowerFerriteBead()), - self.Block(VoltageTestPoint()) + self.conn.pwr, self.Block(SeriesPowerFerriteBead()), self.Block(VoltageTestPoint()) ) self.pwr = self.connect(self.choke.pwr_out) (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( self.pwr, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), ) self.v3v3 = self.connect(self.reg_3v3.pwr_out) with self.implicit_connect( # 3V3 DOMAIN - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: self.mcu = imp.Block(IoController()) self.mcu.with_mixin(IoControllerWifi()) self.connect(self.conn.pwr_io, self.v3v3) - self.connect(self.mcu.uart.request('ctl'), self.conn.uart) + self.connect(self.mcu.uart.request("ctl"), self.conn.uart) self.sw = self.Block(SwitchMatrix(nrows=3, ncols=2)) - self.connect(self.sw.cols, self.mcu.gpio.request_vector('swc')) - self.connect(self.sw.rows, self.mcu.gpio.request_vector('swr')) + self.connect(self.sw.cols, self.mcu.gpio.request_vector("swc")) + self.connect(self.sw.rows, self.mcu.gpio.request_vector("swr")) - (self.ledr, ), _ = self.chain(self.mcu.gpio.request('ledr'), imp.Block(IndicatorLed(Led.Red))) + (self.ledr,), _ = self.chain(self.mcu.gpio.request("ledr"), imp.Block(IndicatorLed(Led.Red))) self.oled = imp.Block(Er_Oled_096_1_1()) self.i2c_pull = imp.Block(I2cPullup()) - self.i2c = self.mcu.i2c.request('i2c') + self.i2c = self.mcu.i2c.request("i2c") self.connect(self.i2c, self.i2c_pull.i2c, self.oled.i2c) - self.reset = self.mcu.gpio.request('oled_rst') + self.reset = self.mcu.gpio.request("oled_rst") self.connect(self.reset, self.oled.reset) - self.io8_pu = imp.Block(PullupResistor(4.7*kOhm(tol=0.05))) - self.connect(self.mcu.gpio.request('spk'), self.io8_pu.io) # TODO support in chain + self.io8_pu = imp.Block(PullupResistor(4.7 * kOhm(tol=0.05))) + self.connect(self.mcu.gpio.request("spk"), self.io8_pu.io) # TODO support in chain with self.implicit_connect( # 5V DOMAIN - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.pwr, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: (self.spk_dac, self.spk_tp, self.spk_drv, self.spk), self.spk_chain = self.chain( self.io8_pu.io, - imp.Block(LowPassRcDac(1*kOhm(tol=0.05), 5*kHertz(tol=0.5))), + imp.Block(LowPassRcDac(1 * kOhm(tol=0.05), 5 * kHertz(tol=0.5))), self.Block(AnalogTestPoint()), imp.Block(Tpa2005d1(gain=Range.from_tolerance(4, 0.2))), - self.Block(Speaker())) + self.Block(Speaker()), + ) # 1k pullup on the HV side is necessary for ~5v input, 4.7k does not provide a sufficient signal - self.npx_shift = imp.Block(BidirectionaLevelShifter(lv_res=RangeExpr.INF, hv_res=1*kOhm(tol=0.05), - src_hint='lv')) + self.npx_shift = imp.Block( + BidirectionaLevelShifter(lv_res=RangeExpr.INF, hv_res=1 * kOhm(tol=0.05), src_hint="lv") + ) self.connect(self.npx_shift.lv_pwr, self.v3v3) self.connect(self.npx_shift.hv_pwr, self.pwr) - self.connect(self.mcu.gpio.request('npx'), self.npx_shift.lv_io) - (self.npx_tp, self.npx), _ = self.chain(self.npx_shift.hv_io, - self.Block(DigitalTestPoint('npx')), - imp.Block(NeopixelArray(6))) + self.connect(self.mcu.gpio.request("npx"), self.npx_shift.lv_io) + (self.npx_tp, self.npx), _ = self.chain( + self.npx_shift.hv_io, self.Block(DigitalTestPoint("npx")), imp.Block(NeopixelArray(6)) + ) @override def refinements(self) -> Refinements: return super().refinements() + Refinements( instance_refinements=[ - (['mcu'], Esp32c3_Wroom02), - (['reg_3v3'], Ldl1117), - (['conn', 'conn'], JstPhKVertical), - (['spk', 'conn'], JstPhKVertical), + (["mcu"], Esp32c3_Wroom02), + (["reg_3v3"], Ldl1117), + (["conn", "conn"], JstPhKVertical), + (["spk", "conn"], JstPhKVertical), ], instance_values=[ - (['refdes_prefix'], 'D'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'ledr=_GPIO9_STRAP', # use the strapping pin to save on IOs - 'oled_rst=_GPIO2_STRAP_EXT_PU', # not pulled up, affects startup glitching - 'spk=_GPIO8_STRAP_EXT_PU', # use the strapping pin to save on IOs - - 'i2c.sda=18', - 'i2c.scl=17', - - 'swr_2=10', - 'swr_1=13', - 'swr_0=14', - 'swc_1=15', - 'swc_0=5', - - 'ctl.rx=3', - 'ctl.tx=4', - ]), - (['mcu', 'programming'], 'uart-auto'), - (['spk_drv', 'pwr', 'current_draw'], Range(0.0022, 0.08)), # don't run at full power - (['npx', 'pwr', 'current_draw'], Range(0.0036, 0.08)), - (['mcu', 'ic', 'pwr', 'current_draw'], Range(1.0E-6, 0.1)), # assume it doesn't run full bore + (["refdes_prefix"], "D"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "ledr=_GPIO9_STRAP", # use the strapping pin to save on IOs + "oled_rst=_GPIO2_STRAP_EXT_PU", # not pulled up, affects startup glitching + "spk=_GPIO8_STRAP_EXT_PU", # use the strapping pin to save on IOs + "i2c.sda=18", + "i2c.scl=17", + "swr_2=10", + "swr_1=13", + "swr_0=14", + "swc_1=15", + "swc_0=5", + "ctl.rx=3", + "ctl.tx=4", + ], + ), + (["mcu", "programming"], "uart-auto"), + (["spk_drv", "pwr", "current_draw"], Range(0.0022, 0.08)), # don't run at full power + (["npx", "pwr", "current_draw"], Range(0.0036, 0.08)), + (["mcu", "ic", "pwr", "current_draw"], Range(1.0e-6, 0.1)), # assume it doesn't run full bore ], class_refinements=[ (EspProgrammingHeader, EspProgrammingTc2030), @@ -147,13 +153,13 @@ def refinements(self) -> Refinements: (Speaker, ConnectorSpeaker), (Switch, KailhSocket), (Neopixel, Sk6812Mini_E), - (Fpc050Bottom, Fpc050BottomFlip) + (Fpc050Bottom, Fpc050BottomFlip), ], class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), - (Nonstrict3v3Compatible, ['nonstrict_3v3_compatible'], True), - (Er_Oled_096_1_1, ['iref_res', 'resistance'], Range.from_tolerance(470e3, 0.1)), - ] + (CompactKeystone5015, ["lcsc_part"], "C5199798"), + (Nonstrict3v3Compatible, ["nonstrict_3v3_compatible"], True), + (Er_Oled_096_1_1, ["iref_res", "resistance"], Range.from_tolerance(470e3, 0.1)), + ], ) diff --git a/examples/test_esp_programmer.py b/examples/test_esp_programmer.py index f128902b4..a940ef334 100644 --- a/examples/test_esp_programmer.py +++ b/examples/test_esp_programmer.py @@ -6,86 +6,85 @@ class EspProgrammerTc2030Inline(Connector, Block): - """UART connector, follows the TXD, RXD, GND, +5 pinning of cheap CP2102 dongles.""" - def __init__(self, *, pwr_current_draw: RangeLike = (0, 0)*mAmp): - super().__init__() - self.conn = self.Block(PinHeader254DualShroudedInline(6)) + """UART connector, follows the TXD, RXD, GND, +5 pinning of cheap CP2102 dongles.""" - self.uart = self.Port(UartPort.empty(), [InOut]) - # note that RX and TX here are from the connected device, so they're flipped from the CP2102's view - self.connect(self.uart.rx, self.conn.pins.request('4').adapt_to(DigitalSink())) - self.connect(self.uart.tx, self.conn.pins.request('3').adapt_to(DigitalSource())) - self.gnd = self.Export(self.conn.pins.request('5').adapt_to(Ground()), - [Common]) - self.pwr = self.Export(self.conn.pins.request('1').adapt_to(VoltageSink( - current_draw=pwr_current_draw - )), [Power]) + def __init__(self, *, pwr_current_draw: RangeLike = (0, 0) * mAmp): + super().__init__() + self.conn = self.Block(PinHeader254DualShroudedInline(6)) - self.en = self.Export(self.conn.pins.request('6').adapt_to(DigitalSink())) # RTS - self.boot = self.Export(self.conn.pins.request('2').adapt_to(DigitalSink())) # CTS + self.uart = self.Port(UartPort.empty(), [InOut]) + # note that RX and TX here are from the connected device, so they're flipped from the CP2102's view + self.connect(self.uart.rx, self.conn.pins.request("4").adapt_to(DigitalSink())) + self.connect(self.uart.tx, self.conn.pins.request("3").adapt_to(DigitalSource())) + self.gnd = self.Export(self.conn.pins.request("5").adapt_to(Ground()), [Common]) + self.pwr = self.Export( + self.conn.pins.request("1").adapt_to(VoltageSink(current_draw=pwr_current_draw)), [Power] + ) + + self.en = self.Export(self.conn.pins.request("6").adapt_to(DigitalSink())) # RTS + self.boot = self.Export(self.conn.pins.request("2").adapt_to(DigitalSink())) # CTS class EspProgrammer(JlcBoardTop): - """USB UART converter board set up for ESP programming including auto-program circuit.""" - @override - def contents(self) -> None: - super().contents() - self.usb_uart = self.Block(UsbCReceptacle()) - - self.vusb = self.connect(self.usb_uart.pwr) - self.gnd = self.connect(self.usb_uart.gnd) - - # 5v DOMAIN - with self.implicit_connect( - ImplicitConnect(self.vusb, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6)*Volt)) - - self.usbconv = imp.Block(Cp2102()) - (self.usb_esd, ), self.usb_chain = self.chain( - self.usb_uart.usb, imp.Block(UsbEsdDiode()), self.usbconv.usb) - - # for target power only - self.reg_3v3 = imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3v3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.out = imp.Block(EspProgrammerTc2030Inline()) - self.connect(self.usbconv.uart, self.out.uart) - self.auto = imp.Block(EspAutoProgram()) - self.connect(self.usbconv.dtr, self.auto.dtr) - self.connect(self.usbconv.rts, self.auto.rts) - self.connect(self.auto.en, self.out.en) - self.connect(self.auto.boot, self.out.boot) - - (self.led, ), _ = self.chain(self.usbconv.suspend, imp.Block(IndicatorSinkLed(Led.White))) - (self.led_en, ), _ = self.chain(self.usbconv.rts, imp.Block(IndicatorSinkLed(Led.Red))) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg_3v3'], Ap2204k), - ], - instance_values=[ - (['refdes_prefix'], 'U'), # unique refdes for panelization - # 2.2uF generates a 1206, but 4.7uF allows a 0805 - (['reg_3v3', 'out_cap', 'cap', 'capacitance'], Range.from_tolerance(4.7e-6, 0.2)), - ], - class_refinements=[ - ], - class_values=[ - (SelectorArea, ['footprint_area'], Range.from_lower(1.5)), # at least 0402 - (TableBjt, ["part"], "BC846BW 1B"), # default option is OOS - ], - ) + """USB UART converter board set up for ESP programming including auto-program circuit.""" + + @override + def contents(self) -> None: + super().contents() + self.usb_uart = self.Block(UsbCReceptacle()) + + self.vusb = self.connect(self.usb_uart.pwr) + self.gnd = self.connect(self.usb_uart.gnd) + + # 5v DOMAIN + with self.implicit_connect( + ImplicitConnect(self.vusb, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6) * Volt)) + + self.usbconv = imp.Block(Cp2102()) + (self.usb_esd,), self.usb_chain = self.chain(self.usb_uart.usb, imp.Block(UsbEsdDiode()), self.usbconv.usb) + + # for target power only + self.reg_3v3 = imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3v3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.out = imp.Block(EspProgrammerTc2030Inline()) + self.connect(self.usbconv.uart, self.out.uart) + self.auto = imp.Block(EspAutoProgram()) + self.connect(self.usbconv.dtr, self.auto.dtr) + self.connect(self.usbconv.rts, self.auto.rts) + self.connect(self.auto.en, self.out.en) + self.connect(self.auto.boot, self.out.boot) + + (self.led,), _ = self.chain(self.usbconv.suspend, imp.Block(IndicatorSinkLed(Led.White))) + (self.led_en,), _ = self.chain(self.usbconv.rts, imp.Block(IndicatorSinkLed(Led.Red))) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg_3v3"], Ap2204k), + ], + instance_values=[ + (["refdes_prefix"], "U"), # unique refdes for panelization + # 2.2uF generates a 1206, but 4.7uF allows a 0805 + (["reg_3v3", "out_cap", "cap", "capacitance"], Range.from_tolerance(4.7e-6, 0.2)), + ], + class_refinements=[], + class_values=[ + (SelectorArea, ["footprint_area"], Range.from_lower(1.5)), # at least 0402 + (TableBjt, ["part"], "BC846BW 1B"), # default option is OOS + ], + ) class EspProgrammerTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(EspProgrammer) + def test_design(self) -> None: + compile_board_inplace(EspProgrammer) diff --git a/examples/test_fcml.py b/examples/test_fcml.py index ef18e17ae..0187f652a 100644 --- a/examples/test_fcml.py +++ b/examples/test_fcml.py @@ -8,530 +8,603 @@ class PowerOutConnector(Connector, Block): - """Parameterized current draw voltage output connector""" - def __init__(self, current: RangeLike): - super().__init__() - self.conn = self.Block(PassiveConnector()) - self.gnd = self.Export(self.conn.pins.request('1').adapt_to(Ground()), [Common]) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSink( - current_draw=current - )), [Power]) + """Parameterized current draw voltage output connector""" + + def __init__(self, current: RangeLike): + super().__init__() + self.conn = self.Block(PassiveConnector()) + self.gnd = self.Export(self.conn.pins.request("1").adapt_to(Ground()), [Common]) + self.pwr = self.Export(self.conn.pins.request("2").adapt_to(VoltageSink(current_draw=current)), [Power]) class SeriesPowerDiode(DiscreteApplication, KiCadImportableBlock): - """Series diode that propagates voltage""" - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'Device:D' - return {'A': self.pwr_in, 'K': self.pwr_out} + """Series diode that propagates voltage""" + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name == "Device:D" + return {"A": self.pwr_in, "K": self.pwr_out} - def __init__(self, reverse_voltage: RangeLike, current: RangeLike, voltage_drop: RangeLike) -> None: - super().__init__() + def __init__(self, reverse_voltage: RangeLike, current: RangeLike, voltage_drop: RangeLike) -> None: + super().__init__() - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration - self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration + self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration - self.diode = self.Block(Diode( - reverse_voltage=reverse_voltage, current=current, - voltage_drop=voltage_drop - )) + self.diode = self.Block(Diode(reverse_voltage=reverse_voltage, current=current, voltage_drop=voltage_drop)) - self.connect(self.pwr_in, self.diode.anode.adapt_to(VoltageSink( - voltage_limits=(-float('inf'), float('inf')), - current_draw=self.pwr_out.link().current_drawn - ))) - self.connect(self.pwr_out, self.diode.cathode.adapt_to(VoltageSource( - voltage_out=self.pwr_in.link().voltage, # ignore voltage drop - current_limits=Range.all() - ))) + self.connect( + self.pwr_in, + self.diode.anode.adapt_to( + VoltageSink( + voltage_limits=(-float("inf"), float("inf")), current_draw=self.pwr_out.link().current_drawn + ) + ), + ) + self.connect( + self.pwr_out, + self.diode.cathode.adapt_to( + VoltageSource(voltage_out=self.pwr_in.link().voltage, current_limits=Range.all()) # ignore voltage drop + ), + ) class MultilevelSwitchingCell(InternalSubcircuit, KiCadSchematicBlock, GeneratorBlock): - """A switching cell for one level of a multilevel converter, consisting of a high FET, - low FET, gate driver, isolator (if needed), and bootstrap circuit (for the gate driver). - - This is its own block to allow hierarchical replicate in layout. - - Current and voltage are provided via the ports. - - The first cell (closest to the power supply) is different in that: - - it does not generate an isolator, since signals are already ground-referenced - - it does not generate a low-side bootstrap diode and cap, since voltage is provided - - it does not generate a flying capacitor on the input, since that is the input cap""" - def __init__(self, is_first: BoolLike = False, *, - in_voltage: RangeLike, frequency: RangeLike, fet_rds: RangeLike, - gate_res: RangeLike): - super().__init__() - # in is generally towards the supply side, out is towards the inductor side - self.low_in = self.Port(Ground.empty()) - self.low_out = self.Port(VoltageSource.empty()) - self.low_boot_in = self.Port(VoltageSink.empty()) # bootstrap voltage for the prior cell, except if is_first - self.low_boot_out = self.Port(VoltageSource.empty()) # bootstrap voltage for this cell - self.high_in = self.Port(VoltageSink.empty()) - self.high_out = self.Port(VoltageSource.empty()) - # except for high boot they're reversed, out is towards the supply side - self.high_boot_out = self.Port(VoltageSource.empty(), optional=True) - self.high_boot_in = self.Port(VoltageSink.empty()) - - # control signals - self.gnd_ctl = self.Port(Ground.empty()) - self.pwr_ctl = self.Port(VoltageSink.empty()) - self.low_pwm = self.Port(DigitalSink.empty()) - self.high_pwm = self.Port(DigitalSink.empty()) - - self.in_voltage = self.ArgParameter(in_voltage) - self.frequency = self.ArgParameter(frequency) - self.fet_rds = self.ArgParameter(fet_rds) - self.gate_res = self.ArgParameter(gate_res) - - self.is_first = self.ArgParameter(is_first) - self.generator_param(self.is_first, self.high_boot_out.is_connected()) - - @override - def generate(self) -> None: - super().generate() - # control path is still defined in HDL - if self.get(self.is_first): - low_pwm: Port[DigitalLink] = self.low_pwm - high_pwm: Port[DigitalLink] = self.high_pwm - self.gnd_ctl.init_from(Ground()) # ideal port, not connected - self.pwr_ctl.init_from(VoltageSink()) # ideal port, not connected - else: - self.ldo = self.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.1))) - self.connect(self.ldo.gnd, self.low_in) - self.connect(self.ldo.pwr_in, self.low_boot_out) - self.iso = self.Block(DigitalIsolator()) - self.connect(self.iso.gnd_a, self.gnd_ctl) - self.connect(self.iso.pwr_a, self.pwr_ctl) - self.connect(self.iso.gnd_b, self.low_in) - self.connect(self.iso.pwr_b, self.ldo.pwr_out) - self.connect(self.iso.in_a.request(f'high'), self.high_pwm) - self.connect(self.iso.in_a.request(f'low'), self.low_pwm) - high_pwm = self.iso.out_b.request(f'high') - low_pwm = self.iso.out_b.request(f'low') - - self.driver = self.Block(HalfBridgeDriver(False)) - self.connect(self.driver.gnd, self.low_in) - self.connect(self.driver.pwr, self.low_boot_out) - driver_indep = self.driver.with_mixin(HalfBridgeDriverIndependent()) - self.connect(driver_indep.high_in, high_pwm) - self.connect(driver_indep.low_in, low_pwm) - self.connect(self.driver.high_gnd, self.high_out.as_ground((0, 0)*Amp)) # TODO model driver current - - if self.get(self.high_boot_out.is_connected()): # leave port disconnected if not used, to avoid an unsolved interface - self.connect(self.driver.high_pwr, self.high_boot_out) # schematic connected to boot diode - - # size the flying cap for max voltage change at max current - # Q = C dv => C = I*t / dV - MAX_FLYING_CAP_DV_PERCENT = 0.08 - flying_cap_capacitance = self.high_out.link().current_drawn.upper() / self.frequency.lower() / (self.in_voltage.upper() * MAX_FLYING_CAP_DV_PERCENT) - - self.import_kicad( - self.file_path("resources", f"{self.__class__.__name__}_{self.get(self.is_first)}.kicad_sch"), - locals={ - 'fet_model': Fet.NFet( - drain_voltage=self.in_voltage, - drain_current=(0, self.high_out.link().current_drawn.upper()), - gate_voltage=self.low_boot_out.link().voltage, # TODO account for boot diode drop - rds_on=self.fet_rds - ), - 'flying_cap_model': Capacitor( # flying cap - capacitance=(flying_cap_capacitance, float('inf')*Farad), - voltage=self.in_voltage, - exact_capacitance=True - ), - 'boot_diode_model': SeriesPowerDiode( - reverse_voltage=self.in_voltage + self.low_boot_in.link().voltage, # upper bound - current=(0, 0)*Amp, # TODO model current draw, though it's probably negligibly small - voltage_drop=(0, 0.6)*Volt # arbitrary to limit gate voltage droop - ), - 'boot_cap_model': Capacitor( - capacitance=0.1*uFarad(tol=0.2), - voltage=self.low_boot_in.link().voltage - ), - 'gate_res_model': Resistor(self.gate_res), - }, - nodes={ - 'low_gate': self.driver.low_out, - 'high_gate': self.driver.high_out, - 'high_boot_out_node': self.driver.high_pwr - }, - conversions={ - 'low_in': Ground(), # TODO better conventions for Ground vs VoltageSink|Source - 'low_out': VoltageSource( - voltage_out=self.low_in.link().voltage - ), - 'high_in': VoltageSink( - current_draw=self.high_out.link().current_drawn - ), - 'high_out': VoltageSource( - voltage_out=self.low_in.link().voltage - ), - 'low_boot_cap.1': VoltageSink(), - 'high_boot_cap.1': VoltageSink(), - 'low_gate': DigitalSink(), # TODO model gate current draw - 'high_gate': DigitalSink(), # TODO model gate current draw - }) + """A switching cell for one level of a multilevel converter, consisting of a high FET, + low FET, gate driver, isolator (if needed), and bootstrap circuit (for the gate driver). + + This is its own block to allow hierarchical replicate in layout. + + Current and voltage are provided via the ports. + + The first cell (closest to the power supply) is different in that: + - it does not generate an isolator, since signals are already ground-referenced + - it does not generate a low-side bootstrap diode and cap, since voltage is provided + - it does not generate a flying capacitor on the input, since that is the input cap""" + + def __init__( + self, + is_first: BoolLike = False, + *, + in_voltage: RangeLike, + frequency: RangeLike, + fet_rds: RangeLike, + gate_res: RangeLike, + ): + super().__init__() + # in is generally towards the supply side, out is towards the inductor side + self.low_in = self.Port(Ground.empty()) + self.low_out = self.Port(VoltageSource.empty()) + self.low_boot_in = self.Port(VoltageSink.empty()) # bootstrap voltage for the prior cell, except if is_first + self.low_boot_out = self.Port(VoltageSource.empty()) # bootstrap voltage for this cell + self.high_in = self.Port(VoltageSink.empty()) + self.high_out = self.Port(VoltageSource.empty()) + # except for high boot they're reversed, out is towards the supply side + self.high_boot_out = self.Port(VoltageSource.empty(), optional=True) + self.high_boot_in = self.Port(VoltageSink.empty()) + + # control signals + self.gnd_ctl = self.Port(Ground.empty()) + self.pwr_ctl = self.Port(VoltageSink.empty()) + self.low_pwm = self.Port(DigitalSink.empty()) + self.high_pwm = self.Port(DigitalSink.empty()) + + self.in_voltage = self.ArgParameter(in_voltage) + self.frequency = self.ArgParameter(frequency) + self.fet_rds = self.ArgParameter(fet_rds) + self.gate_res = self.ArgParameter(gate_res) + + self.is_first = self.ArgParameter(is_first) + self.generator_param(self.is_first, self.high_boot_out.is_connected()) + + @override + def generate(self) -> None: + super().generate() + # control path is still defined in HDL + if self.get(self.is_first): + low_pwm: Port[DigitalLink] = self.low_pwm + high_pwm: Port[DigitalLink] = self.high_pwm + self.gnd_ctl.init_from(Ground()) # ideal port, not connected + self.pwr_ctl.init_from(VoltageSink()) # ideal port, not connected + else: + self.ldo = self.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.1))) + self.connect(self.ldo.gnd, self.low_in) + self.connect(self.ldo.pwr_in, self.low_boot_out) + self.iso = self.Block(DigitalIsolator()) + self.connect(self.iso.gnd_a, self.gnd_ctl) + self.connect(self.iso.pwr_a, self.pwr_ctl) + self.connect(self.iso.gnd_b, self.low_in) + self.connect(self.iso.pwr_b, self.ldo.pwr_out) + self.connect(self.iso.in_a.request(f"high"), self.high_pwm) + self.connect(self.iso.in_a.request(f"low"), self.low_pwm) + high_pwm = self.iso.out_b.request(f"high") + low_pwm = self.iso.out_b.request(f"low") + + self.driver = self.Block(HalfBridgeDriver(False)) + self.connect(self.driver.gnd, self.low_in) + self.connect(self.driver.pwr, self.low_boot_out) + driver_indep = self.driver.with_mixin(HalfBridgeDriverIndependent()) + self.connect(driver_indep.high_in, high_pwm) + self.connect(driver_indep.low_in, low_pwm) + self.connect(self.driver.high_gnd, self.high_out.as_ground((0, 0) * Amp)) # TODO model driver current + + if self.get( + self.high_boot_out.is_connected() + ): # leave port disconnected if not used, to avoid an unsolved interface + self.connect(self.driver.high_pwr, self.high_boot_out) # schematic connected to boot diode + + # size the flying cap for max voltage change at max current + # Q = C dv => C = I*t / dV + MAX_FLYING_CAP_DV_PERCENT = 0.08 + flying_cap_capacitance = ( + self.high_out.link().current_drawn.upper() + / self.frequency.lower() + / (self.in_voltage.upper() * MAX_FLYING_CAP_DV_PERCENT) + ) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}_{self.get(self.is_first)}.kicad_sch"), + locals={ + "fet_model": Fet.NFet( + drain_voltage=self.in_voltage, + drain_current=(0, self.high_out.link().current_drawn.upper()), + gate_voltage=self.low_boot_out.link().voltage, # TODO account for boot diode drop + rds_on=self.fet_rds, + ), + "flying_cap_model": Capacitor( # flying cap + capacitance=(flying_cap_capacitance, float("inf") * Farad), + voltage=self.in_voltage, + exact_capacitance=True, + ), + "boot_diode_model": SeriesPowerDiode( + reverse_voltage=self.in_voltage + self.low_boot_in.link().voltage, # upper bound + current=(0, 0) * Amp, # TODO model current draw, though it's probably negligibly small + voltage_drop=(0, 0.6) * Volt, # arbitrary to limit gate voltage droop + ), + "boot_cap_model": Capacitor(capacitance=0.1 * uFarad(tol=0.2), voltage=self.low_boot_in.link().voltage), + "gate_res_model": Resistor(self.gate_res), + }, + nodes={ + "low_gate": self.driver.low_out, + "high_gate": self.driver.high_out, + "high_boot_out_node": self.driver.high_pwr, + }, + conversions={ + "low_in": Ground(), # TODO better conventions for Ground vs VoltageSink|Source + "low_out": VoltageSource(voltage_out=self.low_in.link().voltage), + "high_in": VoltageSink(current_draw=self.high_out.link().current_drawn), + "high_out": VoltageSource(voltage_out=self.low_in.link().voltage), + "low_boot_cap.1": VoltageSink(), + "high_boot_cap.1": VoltageSink(), + "low_gate": DigitalSink(), # TODO model gate current draw + "high_gate": DigitalSink(), # TODO model gate current draw + }, + ) class FcmlPowerPath(InternalSubcircuit, GeneratorBlock): - """FCML power path that accounts for inductor scaling behavior - TODO: Is there a way to unify this with BuckConverterPowerPath? - This basically completely duplicates it, but adds a scaling factor that doesn't exist there - """ - def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequency: RangeLike, - output_current: RangeLike, sw_current_limits: RangeLike, *, - input_voltage_ripple: FloatLike, - output_voltage_ripple: FloatLike, - efficiency: RangeLike = (0.9, 1.0), # from TI reference - dutycycle_limit: RangeLike = (0.1, 0.9), - ripple_ratio: RangeLike = Range.all(), - inductor_scale: FloatLike = 1.0): # arbitrary - super().__init__() - - self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # models the input cap only - self.pwr_out = self.Port(VoltageSource.empty()) # models the output cap and inductor power source - self.switch = self.Port(VoltageSink.empty()) # current draw defined as average - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input_voltage = self.ArgParameter(input_voltage) - self.output_voltage = self.ArgParameter(output_voltage) - self.frequency = self.ArgParameter(frequency) - self.output_current = self.ArgParameter(output_current) - self.sw_current_limits = self.ArgParameter(sw_current_limits) - - self.efficiency = self.ArgParameter(efficiency) - self.input_voltage_ripple = self.ArgParameter(input_voltage_ripple) - self.output_voltage_ripple = self.ArgParameter(output_voltage_ripple) - self.dutycycle_limit = self.ArgParameter(dutycycle_limit) - self.ripple_ratio = self.ArgParameter(ripple_ratio) # only used to force a ripple ratio at the actual currents - self.inductor_scale = self.ArgParameter(inductor_scale) - - self.generator_param(self.input_voltage, self.output_voltage, self.frequency, self.output_current, - self.sw_current_limits, self.input_voltage_ripple, self.output_voltage_ripple, - self.efficiency, self.dutycycle_limit, self.ripple_ratio, self.inductor_scale) - - self.actual_dutycycle = self.Parameter(RangeExpr()) - self.actual_inductor_current_ripple = self.Parameter(RangeExpr()) - - @override - def generate(self) -> None: - super().generate() - inductor_scale = self.get(self.inductor_scale) - values = BuckConverterPowerPath._calculate_parameters( - self.get(self.input_voltage), self.get(self.output_voltage), - self.get(self.frequency), self.get(self.output_current), - self.get(self.sw_current_limits), self.get(self.ripple_ratio), - self.get(self.input_voltage_ripple), self.get(self.output_voltage_ripple), - efficiency=self.get(self.efficiency), - dutycycle_limit=self.get(self.dutycycle_limit)) - self.assign(self.actual_dutycycle, values.dutycycle) - self.require(values.dutycycle == values.effective_dutycycle, "dutycycle outside limit") - - self.inductor = self.Block(Inductor( - inductance=(values.inductance / inductor_scale) * Henry, - current=values.inductor_avg_current, - frequency=self.frequency, - experimental_filter_fn=ExperimentalUserFnPartsTable.serialize_fn( - BuckConverterPowerPath._buck_inductor_filter, values.inductor_avg_current.upper, - values.ripple_scale / inductor_scale, values.min_ripple) - )) - self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance / inductor_scale) - - self.connect(self.switch, self.inductor.a.adapt_to(VoltageSink( - voltage_limits=RangeExpr.ALL, - current_draw=self.pwr_out.link().current_drawn * values.dutycycle - ))) - self.connect(self.pwr_out, self.inductor.b.adapt_to(VoltageSource( - voltage_out=self.output_voltage, - current_limits=BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits, - self.actual_inductor_current_ripple) - ))) - - self.in_cap = self.Block(DecouplingCapacitor( - capacitance=values.input_capacitance * Farad, - exact_capacitance=True - )).connected(self.gnd, self.pwr_in) - self.out_cap = self.Block(DecouplingCapacitor( - capacitance=(Range.exact(float('inf')) * Farad).hull( - (values.output_capacitance_scale * self.actual_inductor_current_ripple.upper().max(values.min_ripple))), - exact_capacitance=True - )).connected(self.gnd, self.pwr_out) + """FCML power path that accounts for inductor scaling behavior + TODO: Is there a way to unify this with BuckConverterPowerPath? + This basically completely duplicates it, but adds a scaling factor that doesn't exist there + """ + + def __init__( + self, + input_voltage: RangeLike, + output_voltage: RangeLike, + frequency: RangeLike, + output_current: RangeLike, + sw_current_limits: RangeLike, + *, + input_voltage_ripple: FloatLike, + output_voltage_ripple: FloatLike, + efficiency: RangeLike = (0.9, 1.0), # from TI reference + dutycycle_limit: RangeLike = (0.1, 0.9), + ripple_ratio: RangeLike = Range.all(), + inductor_scale: FloatLike = 1.0, + ): # arbitrary + super().__init__() + + self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # models the input cap only + self.pwr_out = self.Port(VoltageSource.empty()) # models the output cap and inductor power source + self.switch = self.Port(VoltageSink.empty()) # current draw defined as average + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input_voltage = self.ArgParameter(input_voltage) + self.output_voltage = self.ArgParameter(output_voltage) + self.frequency = self.ArgParameter(frequency) + self.output_current = self.ArgParameter(output_current) + self.sw_current_limits = self.ArgParameter(sw_current_limits) + + self.efficiency = self.ArgParameter(efficiency) + self.input_voltage_ripple = self.ArgParameter(input_voltage_ripple) + self.output_voltage_ripple = self.ArgParameter(output_voltage_ripple) + self.dutycycle_limit = self.ArgParameter(dutycycle_limit) + self.ripple_ratio = self.ArgParameter(ripple_ratio) # only used to force a ripple ratio at the actual currents + self.inductor_scale = self.ArgParameter(inductor_scale) + + self.generator_param( + self.input_voltage, + self.output_voltage, + self.frequency, + self.output_current, + self.sw_current_limits, + self.input_voltage_ripple, + self.output_voltage_ripple, + self.efficiency, + self.dutycycle_limit, + self.ripple_ratio, + self.inductor_scale, + ) + + self.actual_dutycycle = self.Parameter(RangeExpr()) + self.actual_inductor_current_ripple = self.Parameter(RangeExpr()) + + @override + def generate(self) -> None: + super().generate() + inductor_scale = self.get(self.inductor_scale) + values = BuckConverterPowerPath._calculate_parameters( + self.get(self.input_voltage), + self.get(self.output_voltage), + self.get(self.frequency), + self.get(self.output_current), + self.get(self.sw_current_limits), + self.get(self.ripple_ratio), + self.get(self.input_voltage_ripple), + self.get(self.output_voltage_ripple), + efficiency=self.get(self.efficiency), + dutycycle_limit=self.get(self.dutycycle_limit), + ) + self.assign(self.actual_dutycycle, values.dutycycle) + self.require(values.dutycycle == values.effective_dutycycle, "dutycycle outside limit") + + self.inductor = self.Block( + Inductor( + inductance=(values.inductance / inductor_scale) * Henry, + current=values.inductor_avg_current, + frequency=self.frequency, + experimental_filter_fn=ExperimentalUserFnPartsTable.serialize_fn( + BuckConverterPowerPath._buck_inductor_filter, + values.inductor_avg_current.upper, + values.ripple_scale / inductor_scale, + values.min_ripple, + ), + ) + ) + self.assign( + self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance / inductor_scale + ) + + self.connect( + self.switch, + self.inductor.a.adapt_to( + VoltageSink( + voltage_limits=RangeExpr.ALL, current_draw=self.pwr_out.link().current_drawn * values.dutycycle + ) + ), + ) + self.connect( + self.pwr_out, + self.inductor.b.adapt_to( + VoltageSource( + voltage_out=self.output_voltage, + current_limits=BuckConverterPowerPath._ilim_expr( + self.inductor.actual_current_rating, self.sw_current_limits, self.actual_inductor_current_ripple + ), + ) + ), + ) + + self.in_cap = self.Block( + DecouplingCapacitor(capacitance=values.input_capacitance * Farad, exact_capacitance=True) + ).connected(self.gnd, self.pwr_in) + self.out_cap = self.Block( + DecouplingCapacitor( + capacitance=(Range.exact(float("inf")) * Farad).hull( + ( + values.output_capacitance_scale + * self.actual_inductor_current_ripple.upper().max(values.min_ripple) + ) + ), + exact_capacitance=True, + ) + ).connected(self.gnd, self.pwr_out) class DiscreteMutlilevelBuckConverter(PowerConditioner, GeneratorBlock): - """Flying capacitor multilevel buck converter. Trades more switches for smaller inductor size: - for number of levels N, inductor value is reduced by a factor of (N-1)^2. - 2 levels is standard switching converter (1 pair of switches in a synchronous topology). - Each additional level adds another switch pair. - Even number of levels generally have good balancing, odd number of levels may have balancing issues. - - Controls are broken out at the top level, at logic level referenced to the converter ground. - Requires both a power voltage source and gate drive voltage source. - - Instead of a target output voltage (since the output is controlled by external PWMs), this takes - in operating duty ratios for component sizing. Current is calculated by current drawn at the output. - - Generates a bootstrap capacitor ladder to generate the gate drive voltages for each switch. - Generates a gate driver for each switch pair, in normal operation each gate driver should be - either be high or low (but not both or none). - Generates a digital isolator for each gate driver that is offset from ground. - """ - def __init__(self, levels: IntLike, ratios: RangeLike, frequency: RangeLike, *, - ripple_ratio: RangeLike = (0.2, 0.5), fet_rds: RangeLike = (0, 0.1)*Ohm): - super().__init__() - self.pwr_in = self.Port(VoltageSink.empty()) - self.pwr_out = self.Port(VoltageSource.empty()) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.pwr_gate = self.Port(VoltageSink.empty()) - self.pwr_ctl = self.Port(VoltageSink.empty()) - self.pwms = self.Port(Vector(DigitalSink.empty())) - - self.frequency = self.ArgParameter(frequency) - self.ripple_ratio = self.ArgParameter(ripple_ratio) - self.fet_rds = self.ArgParameter(fet_rds) - - self.levels = self.ArgParameter(levels) - self.ratios = self.ArgParameter(ratios) - self.generator_param(self.levels, self.ratios) - - @override - def generate(self) -> None: - super().generate() - levels = self.get(self.levels) - assert levels >= 2, "levels must be 2 or more" - self.power_path = self.Block(FcmlPowerPath( - self.pwr_in.link().voltage, self.pwr_in.link().voltage * self.get(self.ratios), self.frequency, - self.pwr_out.link().current_drawn, Range.exact(0), - input_voltage_ripple=250*mVolt, - output_voltage_ripple=25*mVolt, # TODO plumb through to user config - ripple_ratio=self.ripple_ratio, - dutycycle_limit=(0, 1), - inductor_scale=(levels - 1)**2 - )) - self.connect(self.power_path.pwr_in, self.pwr_in) - self.connect(self.power_path.pwr_out, self.pwr_out) - self.connect(self.power_path.gnd, self.gnd) - - self.sw = ElementDict[MultilevelSwitchingCell]() - last_sw: Optional[MultilevelSwitchingCell] = None - for level in range(levels - 1): - self.sw[level] = sw = self.Block(MultilevelSwitchingCell( - last_sw is None, - in_voltage=self.pwr_in.link().voltage, - frequency=self.frequency, - fet_rds=self.fet_rds, - gate_res=22*Ohm(tol=0.05) - )) - self.connect(sw.gnd_ctl, self.gnd) - self.connect(sw.pwr_ctl, self.pwr_ctl) - self.connect(sw.low_pwm, self.pwms.append_elt(DigitalSink.empty(), f'{level}L')) - self.connect(sw.high_pwm, self.pwms.append_elt(DigitalSink.empty(), f'{level}H')) - if last_sw is None: - self.connect(sw.low_in, self.gnd) - self.connect(sw.high_in, self.pwr_in) - self.connect(sw.low_boot_in, self.pwr_gate) - else: - self.connect(sw.low_in, last_sw.low_out.as_ground((0, 0)*Amp)) # TODO ground current modeling - self.connect(sw.high_in, last_sw.high_out) - self.connect(sw.low_boot_in, last_sw.low_boot_out) - self.connect(sw.high_boot_out, last_sw.high_boot_in) - - last_sw = sw - - assert last_sw is not None - self.connect(last_sw.low_boot_out, last_sw.high_boot_in) - self.sw_merge = self.Block(MergedVoltageSource()).connected_from( - last_sw.low_out, last_sw.high_out - ) - self.connect(self.sw_merge.pwr_out, self.power_path.switch) + """Flying capacitor multilevel buck converter. Trades more switches for smaller inductor size: + for number of levels N, inductor value is reduced by a factor of (N-1)^2. + 2 levels is standard switching converter (1 pair of switches in a synchronous topology). + Each additional level adds another switch pair. + Even number of levels generally have good balancing, odd number of levels may have balancing issues. + + Controls are broken out at the top level, at logic level referenced to the converter ground. + Requires both a power voltage source and gate drive voltage source. + + Instead of a target output voltage (since the output is controlled by external PWMs), this takes + in operating duty ratios for component sizing. Current is calculated by current drawn at the output. + + Generates a bootstrap capacitor ladder to generate the gate drive voltages for each switch. + Generates a gate driver for each switch pair, in normal operation each gate driver should be + either be high or low (but not both or none). + Generates a digital isolator for each gate driver that is offset from ground. + """ + + def __init__( + self, + levels: IntLike, + ratios: RangeLike, + frequency: RangeLike, + *, + ripple_ratio: RangeLike = (0.2, 0.5), + fet_rds: RangeLike = (0, 0.1) * Ohm, + ): + super().__init__() + self.pwr_in = self.Port(VoltageSink.empty()) + self.pwr_out = self.Port(VoltageSource.empty()) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.pwr_gate = self.Port(VoltageSink.empty()) + self.pwr_ctl = self.Port(VoltageSink.empty()) + self.pwms = self.Port(Vector(DigitalSink.empty())) + + self.frequency = self.ArgParameter(frequency) + self.ripple_ratio = self.ArgParameter(ripple_ratio) + self.fet_rds = self.ArgParameter(fet_rds) + + self.levels = self.ArgParameter(levels) + self.ratios = self.ArgParameter(ratios) + self.generator_param(self.levels, self.ratios) + + @override + def generate(self) -> None: + super().generate() + levels = self.get(self.levels) + assert levels >= 2, "levels must be 2 or more" + self.power_path = self.Block( + FcmlPowerPath( + self.pwr_in.link().voltage, + self.pwr_in.link().voltage * self.get(self.ratios), + self.frequency, + self.pwr_out.link().current_drawn, + Range.exact(0), + input_voltage_ripple=250 * mVolt, + output_voltage_ripple=25 * mVolt, # TODO plumb through to user config + ripple_ratio=self.ripple_ratio, + dutycycle_limit=(0, 1), + inductor_scale=(levels - 1) ** 2, + ) + ) + self.connect(self.power_path.pwr_in, self.pwr_in) + self.connect(self.power_path.pwr_out, self.pwr_out) + self.connect(self.power_path.gnd, self.gnd) + + self.sw = ElementDict[MultilevelSwitchingCell]() + last_sw: Optional[MultilevelSwitchingCell] = None + for level in range(levels - 1): + self.sw[level] = sw = self.Block( + MultilevelSwitchingCell( + last_sw is None, + in_voltage=self.pwr_in.link().voltage, + frequency=self.frequency, + fet_rds=self.fet_rds, + gate_res=22 * Ohm(tol=0.05), + ) + ) + self.connect(sw.gnd_ctl, self.gnd) + self.connect(sw.pwr_ctl, self.pwr_ctl) + self.connect(sw.low_pwm, self.pwms.append_elt(DigitalSink.empty(), f"{level}L")) + self.connect(sw.high_pwm, self.pwms.append_elt(DigitalSink.empty(), f"{level}H")) + if last_sw is None: + self.connect(sw.low_in, self.gnd) + self.connect(sw.high_in, self.pwr_in) + self.connect(sw.low_boot_in, self.pwr_gate) + else: + self.connect(sw.low_in, last_sw.low_out.as_ground((0, 0) * Amp)) # TODO ground current modeling + self.connect(sw.high_in, last_sw.high_out) + self.connect(sw.low_boot_in, last_sw.low_boot_out) + self.connect(sw.high_boot_out, last_sw.high_boot_in) + + last_sw = sw + + assert last_sw is not None + self.connect(last_sw.low_boot_out, last_sw.high_boot_in) + self.sw_merge = self.Block(MergedVoltageSource()).connected_from(last_sw.low_out, last_sw.high_out) + self.connect(self.sw_merge.pwr_out, self.power_path.switch) class Fcml(JlcBoardTop): - """FPGA + FCML (flying cpacitor multilevel converter) test circuit, - plus a bunch of other hardware blocks to test like RP2040""" - @override - def contents(self) -> None: - super().contents() - - self.usb_mcu = self.Block(UsbCReceptacle()) - self.usb_fpga = self.Block(UsbCReceptacle()) - - self.conv_in = self.Block(LipoConnector(voltage=20*Volt(tol=0), actual_voltage=20*Volt(tol=0))) - - self.vusb_merge = self.Block(MergedVoltageSource()).connected_from( - self.usb_mcu.pwr, self.usb_fpga.pwr) - self.vusb = self.connect(self.vusb_merge.pwr_out) - self.gnd = self.connect(self.usb_mcu.gnd, self.usb_fpga.gnd, self.conv_in.gnd) - - self.tp_vusb = self.Block(VoltageTestPoint()).connected(self.vusb_merge.pwr_out) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb_mcu.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.vusb, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - (self.reg_vgate, self.tp_vgate), _ = self.chain( - self.vusb, - imp.Block(BoostConverter(output_voltage=9*Volt(tol=0.1))), - self.Block(VoltageTestPoint()) - ) - self.vgate = self.connect(self.reg_vgate.pwr_out) - - self.conv = imp.Block(DiscreteMutlilevelBuckConverter( - 4, (0.15, 0.5), 100*kHertz(tol=0), - ripple_ratio=(0.1, 0.4), fet_rds=(0, 0.010)*Ohm - )) - self.conv_out = imp.Block(PowerOutConnector((0, 3)*Amp)) - self.connect(self.conv.pwr_in, self.conv_in.pwr) - self.connect(self.conv.pwr_out, self.conv_out.pwr) - self.connect(self.conv.pwr_gate, self.vgate) - self.connect(self.conv.pwr_ctl, self.v3v3) - self.tp_conv_out = self.Block(VoltageTestPoint()).connected(self.conv.pwr_out) - self.tp_conv_gnd = self.Block(GroundTestPoint()).connected(self.conv.gnd) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - # FPGA BLOCK - self.fpga = imp.Block(Ice40up5k_Sg48()) - (self.cdone, ), _ = self.chain(self.fpga.cdone, imp.Block(IndicatorLed())) - (self.fpga_osc, ), _ = self.chain(imp.Block(Oscillator(48*MHertz(tol=0.005))), self.fpga.gpio.request('osc')) - - (self.fpga_sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.fpga.gpio.request('sw')) - (self.fpga_led, ), _ = self.chain(self.fpga.gpio.request_vector('led'), imp.Block(IndicatorLedArray(4))) - - # need to name the USB chain so the USB net has the _N and _P postfix for differential traces - (self.usb_fpga_bitbang, self.usb_fpga_esd), self.usb_fpga_chain = self.chain( - imp.Block(UsbBitBang()).connected_from( - self.fpga.gpio.request('usb_dp_pull'), self.fpga.gpio.request('usb_dp'), self.fpga.gpio.request('usb_dm')), - imp.Block(UsbEsdDiode()), - self.usb_fpga.usb) - - # MICROCONTROLLER BLOCK - self.mcu = imp.Block(IoController()) - - (self.mcu_sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) - (self.mcu_leds, ), _ = self.chain(self.mcu.gpio.request_vector('led'), imp.Block(IndicatorLedArray(4))) - - (self.usb_mcu_esd, ), self.usb_mcu_chain = self.chain( - self.mcu.usb.request('usb'), imp.Block(UsbEsdDiode()), self.usb_mcu.usb) - - self.tp_fpga = ElementDict[DigitalTestPoint]() - for i in range(4): - (self.tp_fpga[i],), _ = self.chain(self.mcu.gpio.request(f'fpga{i}'), - self.Block(DigitalTestPoint()), - self.fpga.gpio.request(f'mcu{i}')) - - # FCML CONTROL BLOCK - (self.pwm_filter, self.tp_pwm), _ = self.chain( - self.fpga.gpio.request_vector('pwm'), - self.Block(DigitalArrayTestPoint()), - imp.Block(DigitalLowPassRcArray(150*Ohm(tol=0.05), 7*MHertz(tol=0.2))), - self.conv.pwms) - - # SENSING - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - div_model = VoltageSenseDivider(full_scale_voltage=1.5*Volt(tol=0.05), impedance=(100, 1000)*Ohm) - (self.conv_in_sense, ), _ = self.chain(self.conv.pwr_in, imp.Block(div_model), - self.mcu.adc.request('conv_in_sense')) - (self.conv_out_sense, ), _ = self.chain(self.conv.pwr_out, imp.Block(div_model), - self.mcu.adc.request('conv_out_sense')) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Rp2040), - (['reg_3v3'], Ldl1117), - (['fpga', 'vcc_reg'], Lp5907), - (['reg_vgate'], Ap3012), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'sw=29', - 'led_0=34', - 'led_1=35', - 'led_2=36', - 'led_3=37', - - 'conv_in_sense=38', - 'conv_out_sense=39', - - 'fpga0=14', - 'fpga1=13', - 'fpga2=12', - 'fpga3=11', - ]), - (['mcu', 'swd_swo_pin'], 'GPIO0'), # UART0 TX - (['mcu', 'swd_tdi_pin'], 'GPIO1'), # UART0 RX - (['fpga', 'pin_assigns'], [ - 'osc=IOB_45a_G1', # use a global buffer input for clock - 'sw=32', - 'led_0=21', - 'led_1=20', - 'led_2=19', - 'led_3=18', - - 'pwm_0H=48', - 'pwm_0L=47', - 'pwm_1H=46', - 'pwm_1L=45', - 'pwm_2H=44', - 'pwm_2L=43', - - 'usb_dm=25', - 'usb_dp=26', - 'usb_dp_pull=27', - - 'mcu0=2', - 'mcu1=3', - 'mcu2=4', - 'mcu3=6', - ]), - - # flying caps need to be beefier for high current rating (which isn't modeled) - (['conv', 'sw[1]', 'cap', 'footprint_spec'], 'Capacitor_SMD:C_1206_3216Metric'), - (['conv', 'sw[2]', 'cap', 'footprint_spec'], ParamValue(['conv', 'sw[1]', 'cap', 'footprint_spec'])), - - # JLC does not have frequency specs, must be checked TODO - (['conv', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - (['reg_vgate', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - - # a bugfix for the SPI flash current draw increased the current beyond the USB port's capabilities - # this hack-patch keeps the example building - (['vusb', 'current_drawn'], Range(0.0311, 0.500)), - ], - class_refinements=[ - (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified - (TestPoint, CompactKeystone5015), - (HalfBridgeDriver, Ir2301), - (DigitalIsolator, Cbmud1200l), - (LinearRegulator, Ap2204k), # for all the switching cells - (UsbEsdDiode, Pgb102st23), # for common parts with the rest of the panel - ], - class_values=[ - (Fet, ['footprint_spec'], 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm'), # don't seem to be alternatives - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - # for compatibility, this board was laid out before derating was supported and does not compile otherwise - (Capacitor, ["voltage_rating_derating"], 1.0), - ], - ) + """FPGA + FCML (flying cpacitor multilevel converter) test circuit, + plus a bunch of other hardware blocks to test like RP2040""" + + @override + def contents(self) -> None: + super().contents() + + self.usb_mcu = self.Block(UsbCReceptacle()) + self.usb_fpga = self.Block(UsbCReceptacle()) + + self.conv_in = self.Block(LipoConnector(voltage=20 * Volt(tol=0), actual_voltage=20 * Volt(tol=0))) + + self.vusb_merge = self.Block(MergedVoltageSource()).connected_from(self.usb_mcu.pwr, self.usb_fpga.pwr) + self.vusb = self.connect(self.vusb_merge.pwr_out) + self.gnd = self.connect(self.usb_mcu.gnd, self.usb_fpga.gnd, self.conv_in.gnd) + + self.tp_vusb = self.Block(VoltageTestPoint()).connected(self.vusb_merge.pwr_out) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb_mcu.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.vusb, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + (self.reg_vgate, self.tp_vgate), _ = self.chain( + self.vusb, imp.Block(BoostConverter(output_voltage=9 * Volt(tol=0.1))), self.Block(VoltageTestPoint()) + ) + self.vgate = self.connect(self.reg_vgate.pwr_out) + + self.conv = imp.Block( + DiscreteMutlilevelBuckConverter( + 4, (0.15, 0.5), 100 * kHertz(tol=0), ripple_ratio=(0.1, 0.4), fet_rds=(0, 0.010) * Ohm + ) + ) + self.conv_out = imp.Block(PowerOutConnector((0, 3) * Amp)) + self.connect(self.conv.pwr_in, self.conv_in.pwr) + self.connect(self.conv.pwr_out, self.conv_out.pwr) + self.connect(self.conv.pwr_gate, self.vgate) + self.connect(self.conv.pwr_ctl, self.v3v3) + self.tp_conv_out = self.Block(VoltageTestPoint()).connected(self.conv.pwr_out) + self.tp_conv_gnd = self.Block(GroundTestPoint()).connected(self.conv.gnd) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + # FPGA BLOCK + self.fpga = imp.Block(Ice40up5k_Sg48()) + (self.cdone,), _ = self.chain(self.fpga.cdone, imp.Block(IndicatorLed())) + (self.fpga_osc,), _ = self.chain( + imp.Block(Oscillator(48 * MHertz(tol=0.005))), self.fpga.gpio.request("osc") + ) + + (self.fpga_sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.fpga.gpio.request("sw")) + (self.fpga_led,), _ = self.chain(self.fpga.gpio.request_vector("led"), imp.Block(IndicatorLedArray(4))) + + # need to name the USB chain so the USB net has the _N and _P postfix for differential traces + (self.usb_fpga_bitbang, self.usb_fpga_esd), self.usb_fpga_chain = self.chain( + imp.Block(UsbBitBang()).connected_from( + self.fpga.gpio.request("usb_dp_pull"), + self.fpga.gpio.request("usb_dp"), + self.fpga.gpio.request("usb_dm"), + ), + imp.Block(UsbEsdDiode()), + self.usb_fpga.usb, + ) + + # MICROCONTROLLER BLOCK + self.mcu = imp.Block(IoController()) + + (self.mcu_sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) + (self.mcu_leds,), _ = self.chain(self.mcu.gpio.request_vector("led"), imp.Block(IndicatorLedArray(4))) + + (self.usb_mcu_esd,), self.usb_mcu_chain = self.chain( + self.mcu.usb.request("usb"), imp.Block(UsbEsdDiode()), self.usb_mcu.usb + ) + + self.tp_fpga = ElementDict[DigitalTestPoint]() + for i in range(4): + (self.tp_fpga[i],), _ = self.chain( + self.mcu.gpio.request(f"fpga{i}"), self.Block(DigitalTestPoint()), self.fpga.gpio.request(f"mcu{i}") + ) + + # FCML CONTROL BLOCK + (self.pwm_filter, self.tp_pwm), _ = self.chain( + self.fpga.gpio.request_vector("pwm"), + self.Block(DigitalArrayTestPoint()), + imp.Block(DigitalLowPassRcArray(150 * Ohm(tol=0.05), 7 * MHertz(tol=0.2))), + self.conv.pwms, + ) + + # SENSING + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + div_model = VoltageSenseDivider(full_scale_voltage=1.5 * Volt(tol=0.05), impedance=(100, 1000) * Ohm) + (self.conv_in_sense,), _ = self.chain( + self.conv.pwr_in, imp.Block(div_model), self.mcu.adc.request("conv_in_sense") + ) + (self.conv_out_sense,), _ = self.chain( + self.conv.pwr_out, imp.Block(div_model), self.mcu.adc.request("conv_out_sense") + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Rp2040), + (["reg_3v3"], Ldl1117), + (["fpga", "vcc_reg"], Lp5907), + (["reg_vgate"], Ap3012), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "sw=29", + "led_0=34", + "led_1=35", + "led_2=36", + "led_3=37", + "conv_in_sense=38", + "conv_out_sense=39", + "fpga0=14", + "fpga1=13", + "fpga2=12", + "fpga3=11", + ], + ), + (["mcu", "swd_swo_pin"], "GPIO0"), # UART0 TX + (["mcu", "swd_tdi_pin"], "GPIO1"), # UART0 RX + ( + ["fpga", "pin_assigns"], + [ + "osc=IOB_45a_G1", # use a global buffer input for clock + "sw=32", + "led_0=21", + "led_1=20", + "led_2=19", + "led_3=18", + "pwm_0H=48", + "pwm_0L=47", + "pwm_1H=46", + "pwm_1L=45", + "pwm_2H=44", + "pwm_2L=43", + "usb_dm=25", + "usb_dp=26", + "usb_dp_pull=27", + "mcu0=2", + "mcu1=3", + "mcu2=4", + "mcu3=6", + ], + ), + # flying caps need to be beefier for high current rating (which isn't modeled) + (["conv", "sw[1]", "cap", "footprint_spec"], "Capacitor_SMD:C_1206_3216Metric"), + (["conv", "sw[2]", "cap", "footprint_spec"], ParamValue(["conv", "sw[1]", "cap", "footprint_spec"])), + # JLC does not have frequency specs, must be checked TODO + (["conv", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + (["reg_vgate", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + # a bugfix for the SPI flash current draw increased the current beyond the USB port's capabilities + # this hack-patch keeps the example building + (["vusb", "current_drawn"], Range(0.0311, 0.500)), + ], + class_refinements=[ + (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified + (TestPoint, CompactKeystone5015), + (HalfBridgeDriver, Ir2301), + (DigitalIsolator, Cbmud1200l), + (LinearRegulator, Ap2204k), # for all the switching cells + (UsbEsdDiode, Pgb102st23), # for common parts with the rest of the panel + ], + class_values=[ + (Fet, ["footprint_spec"], "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm"), # don't seem to be alternatives + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + # for compatibility, this board was laid out before derating was supported and does not compile otherwise + (Capacitor, ["voltage_rating_derating"], 1.0), + ], + ) class FcmlTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(Fcml) + def test_design(self) -> None: + compile_board_inplace(Fcml) diff --git a/examples/test_high_switch.py b/examples/test_high_switch.py index 82f4e89fd..1a6932474 100644 --- a/examples/test_high_switch.py +++ b/examples/test_high_switch.py @@ -15,358 +15,409 @@ class CalSolCanBlock(Block): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.controller = self.Port(CanTransceiverPort.empty(), [Input]) - self.can = self.Port(CanDiffPort.empty(), optional=True) + self.controller = self.Port(CanTransceiverPort.empty(), [Input]) + self.can = self.Port(CanDiffPort.empty(), optional=True) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.conn = self.Block(CalSolCanConnector()) - self.connect(self.can, self.conn.differential) + self.conn = self.Block(CalSolCanConnector()) + self.connect(self.can, self.conn.differential) - self.can_fuse = self.Block(SeriesPowerFuse(150 * mAmp(tol=0.1))) - self.connect(self.conn.pwr, self.can_fuse.pwr_in) + self.can_fuse = self.Block(SeriesPowerFuse(150 * mAmp(tol=0.1))) + self.connect(self.conn.pwr, self.can_fuse.pwr_in) - with self.implicit_connect( - ImplicitConnect(self.can_fuse.pwr_out, [Power]), - ImplicitConnect(self.conn.gnd, [Common]), - ) as imp: - self.reg = imp.Block(Ap2204k(5*Volt(tol=0.05))) # TODO: replace with generic LinearRegulator? + with self.implicit_connect( + ImplicitConnect(self.can_fuse.pwr_out, [Power]), + ImplicitConnect(self.conn.gnd, [Common]), + ) as imp: + self.reg = imp.Block(Ap2204k(5 * Volt(tol=0.05))) # TODO: replace with generic LinearRegulator? - self.esd = imp.Block(CanEsdDiode()) - self.connect(self.esd.can, self.can) + self.esd = imp.Block(CanEsdDiode()) + self.connect(self.esd.can, self.can) - with self.implicit_connect( # Logic-side implicit - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.transceiver = imp.Block(Iso1050dub()) - self.connect(self.transceiver.controller, self.controller) - self.connect(self.transceiver.can, self.can) - self.connect(self.transceiver.can_pwr, self.reg.pwr_out) - self.connect(self.transceiver.can_gnd, self.conn.gnd) + with self.implicit_connect( # Logic-side implicit + ImplicitConnect(self.pwr, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.transceiver = imp.Block(Iso1050dub()) + self.connect(self.transceiver.controller, self.controller) + self.connect(self.transceiver.can, self.can) + self.connect(self.transceiver.can_pwr, self.reg.pwr_out) + self.connect(self.transceiver.can_gnd, self.conn.gnd) class CanFuse(PptcFuse, FootprintBlock): - def __init__(self, trip_current: RangeLike = (100, 200)*mAmp): - super().__init__(trip_current) + def __init__(self, trip_current: RangeLike = (100, 200) * mAmp): + super().__init__(trip_current) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.assign(self.actual_trip_current, 150*mAmp(tol=0)) - self.assign(self.actual_hold_current, 50*mAmp(tol=0)) - self.assign(self.actual_voltage_rating, (0, 15)*Volt) + self.assign(self.actual_trip_current, 150 * mAmp(tol=0)) + self.assign(self.actual_hold_current, 50 * mAmp(tol=0)) + self.assign(self.actual_voltage_rating, (0, 15) * Volt) - self.footprint( - 'F', 'Resistor_SMD:R_0603_1608Metric', - { - '1': self.a, - '2': self.b, - }, - part='0ZCM0005FF2G' - ) + self.footprint( + "F", + "Resistor_SMD:R_0603_1608Metric", + { + "1": self.a, + "2": self.b, + }, + part="0ZCM0005FF2G", + ) class CalSolPowerConnector(Connector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSource( - voltage_out=12 * Volt(tol=0.1), - current_limits=(0, 3) * Amp # TODO get actual limits from LVPDB? - )) - self.gnd = self.Port(Ground()) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'J', 'calisco:Molex_DuraClik_vert_3pin', - { - '1': self.gnd, - '2': self.pwr, - '3': self.gnd, - }, - mfr='Molex', part='5600200320' - ) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port( + VoltageSource( + voltage_out=12 * Volt(tol=0.1), current_limits=(0, 3) * Amp # TODO get actual limits from LVPDB? + ) + ) + self.gnd = self.Port(Ground()) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "J", + "calisco:Molex_DuraClik_vert_3pin", + { + "1": self.gnd, + "2": self.pwr, + "3": self.gnd, + }, + mfr="Molex", + part="5600200320", + ) class CalSolCanConnector(Connector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSource( - voltage_out=(7, 14) * Volt, # TODO get limits from CAN power brick? - current_limits=(0, 0.15) * Amp # TODO get actual limits from ??? - )) - self.gnd = self.Port(Ground()) - self.differential = self.Port(CanDiffPort(), [Output]) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'J', 'calisco:Molex_DuraClik_vert_5pin', - { - # 1 is SHLD - '2': self.pwr, - '3': self.gnd, - '4': self.differential.canh, - '5': self.differential.canl, - }, - mfr='Molex', part='5600200520' - ) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port( + VoltageSource( + voltage_out=(7, 14) * Volt, # TODO get limits from CAN power brick? + current_limits=(0, 0.15) * Amp, # TODO get actual limits from ??? + ) + ) + self.gnd = self.Port(Ground()) + self.differential = self.Port(CanDiffPort(), [Output]) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "J", + "calisco:Molex_DuraClik_vert_5pin", + { + # 1 is SHLD + "2": self.pwr, + "3": self.gnd, + "4": self.differential.canh, + "5": self.differential.canl, + }, + mfr="Molex", + part="5600200520", + ) class CalSolCanConnectorRa(Connector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSource( - voltage_out=(7, 14) * Volt, # TODO get limits from CAN power brick? - current_limits=(0, 0.15) * Amp # TODO get actual limits from ??? - )) - self.gnd = self.Port(Ground()) - self.differential = self.Port(CanDiffPort(), [Output]) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'J', 'calisco:Molex_DuraClik_502352_1x05_P2.00mm_Horizontal', - { - # 1 is SHLD - '2': self.pwr, - '3': self.gnd, - '4': self.differential.canh, - '5': self.differential.canl, - }, - mfr='Molex', part='5023520500' - ) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port( + VoltageSource( + voltage_out=(7, 14) * Volt, # TODO get limits from CAN power brick? + current_limits=(0, 0.15) * Amp, # TODO get actual limits from ??? + ) + ) + self.gnd = self.Port(Ground()) + self.differential = self.Port(CanDiffPort(), [Output]) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "J", + "calisco:Molex_DuraClik_502352_1x05_P2.00mm_Horizontal", + { + # 1 is SHLD + "2": self.pwr, + "3": self.gnd, + "4": self.differential.canh, + "5": self.differential.canl, + }, + mfr="Molex", + part="5023520500", + ) class M12CanConnector(Connector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSource( - voltage_out=(7, 14) * Volt, # TODO get limits from CAN power brick? - current_limits=(0, 0.15) * Amp # TODO get actual limits from ??? - )) - self.gnd = self.Port(Ground()) - self.differential = self.Port(CanDiffPort(), [Output]) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'J', 'calisco:PhoenixContact_M12-5_Pin_SACC-DSIV-MS-5CON-L90', - { - # 1 is SHLD - '2': self.pwr, - '3': self.gnd, - '4': self.differential.canh, - '5': self.differential.canl, - }, - mfr='Phoenix Contact', part='SACC-DSIV-MS-5CON-L90' - ) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port( + VoltageSource( + voltage_out=(7, 14) * Volt, # TODO get limits from CAN power brick? + current_limits=(0, 0.15) * Amp, # TODO get actual limits from ??? + ) + ) + self.gnd = self.Port(Ground()) + self.differential = self.Port(CanDiffPort(), [Output]) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "J", + "calisco:PhoenixContact_M12-5_Pin_SACC-DSIV-MS-5CON-L90", + { + # 1 is SHLD + "2": self.pwr, + "3": self.gnd, + "4": self.differential.canh, + "5": self.differential.canl, + }, + mfr="Phoenix Contact", + part="SACC-DSIV-MS-5CON-L90", + ) class LightsConnector(Connector, FootprintBlock): - def __init__(self, current_draw: RangeLike = RangeExpr()) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSink(), [Power]) # unused, doesn't draw anything - self.gnd = self.Port(Ground(), [Common]) - self.out = ElementDict[VoltageSink]() - for i in range(2): - self.out[i] = self.Port(VoltageSink(current_draw=current_draw)) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'J', 'calisco:Molex_DuraClik_vert_4pin', - { - '1': self.pwr, - '2': self.out[0], - '3': self.out[1], - '4': self.gnd, - }, - mfr='Molex', part='5600200420' - ) + def __init__(self, current_draw: RangeLike = RangeExpr()) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink(), [Power]) # unused, doesn't draw anything + self.gnd = self.Port(Ground(), [Common]) + self.out = ElementDict[VoltageSink]() + for i in range(2): + self.out[i] = self.Port(VoltageSink(current_draw=current_draw)) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "J", + "calisco:Molex_DuraClik_vert_4pin", + { + "1": self.pwr, + "2": self.out[0], + "3": self.out[1], + "4": self.gnd, + }, + mfr="Molex", + part="5600200420", + ) class LightsDriver(Block): - def __init__(self, current_draw: RangeLike = RangeExpr()) -> None: - super().__init__() + def __init__(self, current_draw: RangeLike = RangeExpr()) -> None: + super().__init__() - self.current_draw = current_draw + self.current_draw = current_draw - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) - self.control = ElementDict[DigitalSink]() + self.control = ElementDict[DigitalSink]() - for i in range(2): - self.control[i] = self.Port(DigitalSink.empty()) + for i in range(2): + self.control[i] = self.Port(DigitalSink.empty()) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.conn = imp.Block(LightsConnector(current_draw=self.current_draw)) - self.drv = ElementDict[Block]() + with self.implicit_connect( + ImplicitConnect(self.pwr, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.conn = imp.Block(LightsConnector(current_draw=self.current_draw)) + self.drv = ElementDict[Block]() - for i in range(2): - driver = self.drv[i] = imp.Block(HighSideSwitch(frequency=(0.1, 10) * kHertz)) - self.connect(self.control[i], driver.control) - self.connect(driver.output, self.conn.out[i]) + for i in range(2): + driver = self.drv[i] = imp.Block(HighSideSwitch(frequency=(0.1, 10) * kHertz)) + self.connect(self.control[i], driver.control) + self.connect(driver.output, self.conn.out[i]) class HighSwitch(BoardTop): - @override - def contents(self) -> None: - super().contents() - - self.pwr_conn = self.Block(CalSolPowerConnector()) - - self.vin = self.connect(self.pwr_conn.pwr) - self.gnd = self.connect(self.pwr_conn.gnd) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.pwr, ), _ = self.chain( - self.vin, - imp.Block(Tps561201(output_voltage=3.3*Volt(tol=0.05)))) - - self.v3v3 = self.connect(self.pwr.pwr_out) - - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.can, ), self.can_chain = self.chain(self.mcu.with_mixin(IoControllerCan()).can.request('can'), - imp.Block(CalSolCanBlock())) - - (self.vsense, ), _ = self.chain( # TODO update to use VoltageSenseDivider - self.vin, - imp.Block(VoltageDivider(output_voltage=3 * Volt(tol=0.15), impedance=(100, 1000) * Ohm)), - self.mcu.adc.request('vsense')) - - self.rgb1 = imp.Block(IndicatorSinkRgbLed()) # CAN RGB - self.connect(self.mcu.gpio.request_vector('rgb1'), self.rgb1.signals) - - self.rgb2 = imp.Block(IndicatorSinkRgbLed()) # system RGB 2 - self.connect(self.mcu.gpio.request_vector('rgb2'), self.rgb2.signals) - - self.limit_light_current = self.Block(ForcedVoltageCurrentDraw((0, 2.5) * Amp)) - self.connect(self.vin, self.limit_light_current.pwr_in) - with self.implicit_connect( - ImplicitConnect(self.limit_light_current.pwr_out, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.light = ElementDict[LightsDriver]() - for i in range(4): - light = self.light[i] = imp.Block(LightsDriver((0, 0.5) * Amp)) - self.connect(self.mcu.gpio.request(f'light_{i}0'), light.control[0]) - self.connect(self.mcu.gpio.request(f'light_{i}1'), light.control[1]) - - for i in range(4, 6): - light = self.light[i] = imp.Block(LightsDriver((0, 3) * Amp)) - self.connect(self.mcu.gpio.request(f'light_{i}0'), light.control[0]) - self.connect(self.mcu.gpio.request(f'light_{i}1'), light.control[1]) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Lpc1549_48), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'can.txd=43', - 'can.rxd=44', - 'vsense=21', - 'rgb1_red=28', - 'rgb1_green=23', - 'rgb1_blue=22', - 'rgb2_red=18', - 'rgb2_green=15', - 'rgb2_blue=13', - 'light_00=12', - 'light_01=8', - 'light_10=7', - 'light_11=6', - 'light_20=4', - 'light_21=3', - 'light_30=2', - 'light_31=1', - 'light_40=48', - 'light_41=47', - 'light_50=46', - 'light_51=45', - ]), - (['mcu', 'swd_swo_pin'], 'PIO0_8'), - # the hold current wasn't modeled at the time of manufacture and turns out to be out of limits - (['can', 'can_fuse', 'fuse', 'actual_hold_current'], Range(0.1, 0.1)), - # JLC does not have frequency specs, must be checked TODO - (['pwr', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - # JLC does not have gate charge spec, so ignore the power calc TODO - (['light[0]', 'drv[0]', 'drv', 'frequency'], Range(0, 0)), - (['light[0]', 'drv[1]', 'drv', 'frequency'], Range(0, 0)), - (['light[1]', 'drv[0]', 'drv', 'frequency'], Range(0, 0)), - (['light[1]', 'drv[1]', 'drv', 'frequency'], Range(0, 0)), - (['light[2]', 'drv[0]', 'drv', 'frequency'], Range(0, 0)), - (['light[2]', 'drv[1]', 'drv', 'frequency'], Range(0, 0)), - (['light[3]', 'drv[0]', 'drv', 'frequency'], Range(0, 0)), - (['light[3]', 'drv[1]', 'drv', 'frequency'], Range(0, 0)), - (['light[4]', 'drv[0]', 'drv', 'frequency'], Range(0, 0)), - (['light[4]', 'drv[1]', 'drv', 'frequency'], Range(0, 0)), - (['light[5]', 'drv[0]', 'drv', 'frequency'], Range(0, 0)), - (['light[5]', 'drv[1]', 'drv', 'frequency'], Range(0, 0)), - - # keep netlist footprints as libraries change - (['light[0]', 'drv[0]', 'drv', 'footprint_spec'], 'Package_TO_SOT_SMD:TO-252-2'), - (['light[0]', 'drv[1]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[1]', 'drv[0]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[1]', 'drv[1]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[2]', 'drv[0]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[2]', 'drv[1]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[3]', 'drv[0]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[3]', 'drv[1]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[4]', 'drv[0]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[4]', 'drv[1]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[5]', 'drv[0]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - (['light[5]', 'drv[1]', 'drv', 'footprint_spec'], ParamValue(['light[0]', 'drv[0]', 'drv', 'footprint_spec'])), - ], - class_refinements=[ - (Fuse, CanFuse) - ], - ) + @override + def contents(self) -> None: + super().contents() + + self.pwr_conn = self.Block(CalSolPowerConnector()) + + self.vin = self.connect(self.pwr_conn.pwr) + self.gnd = self.connect(self.pwr_conn.gnd) + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.pwr,), _ = self.chain(self.vin, imp.Block(Tps561201(output_voltage=3.3 * Volt(tol=0.05)))) + + self.v3v3 = self.connect(self.pwr.pwr_out) + + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.can,), self.can_chain = self.chain( + self.mcu.with_mixin(IoControllerCan()).can.request("can"), imp.Block(CalSolCanBlock()) + ) + + (self.vsense,), _ = self.chain( # TODO update to use VoltageSenseDivider + self.vin, + imp.Block(VoltageDivider(output_voltage=3 * Volt(tol=0.15), impedance=(100, 1000) * Ohm)), + self.mcu.adc.request("vsense"), + ) + + self.rgb1 = imp.Block(IndicatorSinkRgbLed()) # CAN RGB + self.connect(self.mcu.gpio.request_vector("rgb1"), self.rgb1.signals) + + self.rgb2 = imp.Block(IndicatorSinkRgbLed()) # system RGB 2 + self.connect(self.mcu.gpio.request_vector("rgb2"), self.rgb2.signals) + + self.limit_light_current = self.Block(ForcedVoltageCurrentDraw((0, 2.5) * Amp)) + self.connect(self.vin, self.limit_light_current.pwr_in) + with self.implicit_connect( + ImplicitConnect(self.limit_light_current.pwr_out, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.light = ElementDict[LightsDriver]() + for i in range(4): + light = self.light[i] = imp.Block(LightsDriver((0, 0.5) * Amp)) + self.connect(self.mcu.gpio.request(f"light_{i}0"), light.control[0]) + self.connect(self.mcu.gpio.request(f"light_{i}1"), light.control[1]) + + for i in range(4, 6): + light = self.light[i] = imp.Block(LightsDriver((0, 3) * Amp)) + self.connect(self.mcu.gpio.request(f"light_{i}0"), light.control[0]) + self.connect(self.mcu.gpio.request(f"light_{i}1"), light.control[1]) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Lpc1549_48), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "can.txd=43", + "can.rxd=44", + "vsense=21", + "rgb1_red=28", + "rgb1_green=23", + "rgb1_blue=22", + "rgb2_red=18", + "rgb2_green=15", + "rgb2_blue=13", + "light_00=12", + "light_01=8", + "light_10=7", + "light_11=6", + "light_20=4", + "light_21=3", + "light_30=2", + "light_31=1", + "light_40=48", + "light_41=47", + "light_50=46", + "light_51=45", + ], + ), + (["mcu", "swd_swo_pin"], "PIO0_8"), + # the hold current wasn't modeled at the time of manufacture and turns out to be out of limits + (["can", "can_fuse", "fuse", "actual_hold_current"], Range(0.1, 0.1)), + # JLC does not have frequency specs, must be checked TODO + (["pwr", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + # JLC does not have gate charge spec, so ignore the power calc TODO + (["light[0]", "drv[0]", "drv", "frequency"], Range(0, 0)), + (["light[0]", "drv[1]", "drv", "frequency"], Range(0, 0)), + (["light[1]", "drv[0]", "drv", "frequency"], Range(0, 0)), + (["light[1]", "drv[1]", "drv", "frequency"], Range(0, 0)), + (["light[2]", "drv[0]", "drv", "frequency"], Range(0, 0)), + (["light[2]", "drv[1]", "drv", "frequency"], Range(0, 0)), + (["light[3]", "drv[0]", "drv", "frequency"], Range(0, 0)), + (["light[3]", "drv[1]", "drv", "frequency"], Range(0, 0)), + (["light[4]", "drv[0]", "drv", "frequency"], Range(0, 0)), + (["light[4]", "drv[1]", "drv", "frequency"], Range(0, 0)), + (["light[5]", "drv[0]", "drv", "frequency"], Range(0, 0)), + (["light[5]", "drv[1]", "drv", "frequency"], Range(0, 0)), + # keep netlist footprints as libraries change + (["light[0]", "drv[0]", "drv", "footprint_spec"], "Package_TO_SOT_SMD:TO-252-2"), + ( + ["light[0]", "drv[1]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[1]", "drv[0]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[1]", "drv[1]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[2]", "drv[0]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[2]", "drv[1]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[3]", "drv[0]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[3]", "drv[1]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[4]", "drv[0]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[4]", "drv[1]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[5]", "drv[0]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ( + ["light[5]", "drv[1]", "drv", "footprint_spec"], + ParamValue(["light[0]", "drv[0]", "drv", "footprint_spec"]), + ), + ], + class_refinements=[(Fuse, CanFuse)], + ) class HighSwitchTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(HighSwitch) + def test_design(self) -> None: + compile_board_inplace(HighSwitch) diff --git a/examples/test_iot_blinds.py b/examples/test_iot_blinds.py index 0fb2f5413..6af821fdb 100644 --- a/examples/test_iot_blinds.py +++ b/examples/test_iot_blinds.py @@ -9,34 +9,39 @@ class IotRollerBlindsConnector(Block): def __init__(self) -> None: super().__init__() self.conn = self.Block(JstXh(length=6)) - self.gnd = self.Export(self.conn.pins.request('4').adapt_to(Ground())) - self.pwr = self.Export(self.conn.pins.request('1').adapt_to( - VoltageSink.from_gnd(self.gnd, voltage_limits=(4.5, 25)*Volt))) + self.gnd = self.Export(self.conn.pins.request("4").adapt_to(Ground())) + self.pwr = self.Export( + self.conn.pins.request("1").adapt_to(VoltageSink.from_gnd(self.gnd, voltage_limits=(4.5, 25) * Volt)) + ) - self.enca = self.Export(self.conn.pins.request('2').adapt_to(DigitalSource.low_from_supply(self.gnd))) - self.encb = self.Export(self.conn.pins.request('3').adapt_to(DigitalSource.low_from_supply(self.gnd))) + self.enca = self.Export(self.conn.pins.request("2").adapt_to(DigitalSource.low_from_supply(self.gnd))) + self.encb = self.Export(self.conn.pins.request("3").adapt_to(DigitalSource.low_from_supply(self.gnd))) - self.motor2 = self.Export(self.conn.pins.request('5').adapt_to(DigitalSink(current_draw=(0, 0.5)*Amp))) - self.motor1 = self.Export(self.conn.pins.request('6').adapt_to(DigitalSink(current_draw=(0, 0.5)*Amp))) + self.motor2 = self.Export(self.conn.pins.request("5").adapt_to(DigitalSink(current_draw=(0, 0.5) * Amp))) + self.motor1 = self.Export(self.conn.pins.request("6").adapt_to(DigitalSink(current_draw=(0, 0.5) * Amp))) class PowerInConnector(Connector): def __init__(self) -> None: super().__init__() self.conn = self.Block(JstPh()) - self.gnd = self.Export(self.conn.pins.request('1').adapt_to(Ground())) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSource( - voltage_out=(10, 25)*Volt, - current_limits=(0, 1)*Amp, - ))) + self.gnd = self.Export(self.conn.pins.request("1").adapt_to(Ground())) + self.pwr = self.Export( + self.conn.pins.request("2").adapt_to( + VoltageSource( + voltage_out=(10, 25) * Volt, + current_limits=(0, 1) * Amp, + ) + ) + ) class PowerOutConnector(Connector): def __init__(self) -> None: super().__init__() self.conn = self.Block(JstPh()) - self.gnd = self.Export(self.conn.pins.request('1').adapt_to(Ground())) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSink())) + self.gnd = self.Export(self.conn.pins.request("1").adapt_to(Ground())) + self.pwr = self.Export(self.conn.pins.request("2").adapt_to(VoltageSink())) class IotRollerBlinds(JlcBoardTop): @@ -51,6 +56,7 @@ class IotRollerBlinds(JlcBoardTop): 6 motor 1 Motor takes ~12v (stall ~500mA, no-load ~300mA, min start 4v @ 150mA) """ + @override def contents(self) -> None: super().contents() @@ -67,11 +73,11 @@ def contents(self) -> None: # POWER with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.gnd, [Common]), ) as imp: (self.fuse, self.ferrite, self.tp_vin), _ = self.chain( self.vin_raw, - self.Block(SeriesPowerFuse(trip_current=(500, 1000)*mAmp)), + self.Block(SeriesPowerFuse(trip_current=(500, 1000) * mAmp)), self.Block(SeriesPowerFerriteBead()), self.Block(VoltageTestPoint()), ) @@ -79,89 +85,95 @@ def contents(self) -> None: (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( self.vin, - imp.Block(VoltageRegulator(output_voltage=3.3*Volt(tol=0.05))), + imp.Block(VoltageRegulator(output_voltage=3.3 * Volt(tol=0.05))), self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), ) self.v3v3 = self.connect(self.reg_3v3.pwr_out) # 3V3 DOMAIN with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: self.mcu = imp.Block(IoController()) self.mcu.with_mixin(IoControllerWifi()) # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request('led')) + (self.ledr,), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request("led")) - (self.vin_sense, ), _ = self.chain( + (self.vin_sense,), _ = self.chain( self.vin, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('vin_sense') + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("vin_sense"), ) - self.connect(self.conn.enca, self.mcu.gpio.request('enca')) - self.connect(self.conn.encb, self.mcu.gpio.request('encb')) + self.connect(self.conn.enca, self.mcu.gpio.request("enca")) + self.connect(self.conn.encb, self.mcu.gpio.request("encb")) # generic expansion - (self.qwiic_pull, self.qwiic, ), _ = self.chain(self.mcu.i2c.request('qwiic'), - imp.Block(I2cPullup()), - imp.Block(QwiicTarget())) + ( + self.qwiic_pull, + self.qwiic, + ), _ = self.chain(self.mcu.i2c.request("qwiic"), imp.Block(I2cPullup()), imp.Block(QwiicTarget())) # 12V DOMAIN with self.implicit_connect( - ImplicitConnect(self.vin, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.vin, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: - self.drv = imp.Block(Drv8870(current_trip=500*mAmp(tol=0.1))) + self.drv = imp.Block(Drv8870(current_trip=500 * mAmp(tol=0.1))) self.connect(self.drv.vref, self.v3v3) - self.connect(self.mcu.gpio.request('motor1'), self.drv.in1) - self.connect(self.mcu.gpio.request('motor2'), self.drv.in2) + self.connect(self.mcu.gpio.request("motor1"), self.drv.in1) + self.connect(self.mcu.gpio.request("motor2"), self.drv.in2) self.connect(self.drv.out1, self.conn.motor2) self.connect(self.drv.out2, self.conn.motor1) - self._block_diagram_grouping = self.Metadata({ - 'pwr': 'pwr, pwr_out, tp_gnd, fuse, tp_vin, reg_3v3, prot_3v3, tp_3v3, vin_sense', - 'mcu': 'mcu, ledr, als, qwiic, qwiic_pull', - 'app': 'conn, drv', - }) + self._block_diagram_grouping = self.Metadata( + { + "pwr": "pwr, pwr_out, tp_gnd, fuse, tp_vin, reg_3v3, prot_3v3, tp_3v3, vin_sense", + "mcu": "mcu, ledr, als, qwiic, qwiic_pull", + "app": "conn, drv", + } + ) @override def refinements(self) -> Refinements: return super().refinements() + Refinements( instance_refinements=[ - (['mcu'], Esp32c3_Wroom02), - (['reg_3v3'], Tps54202h), - (['drv', 'vm_cap1', 'cap'], AluminumCapacitor), + (["mcu"], Esp32c3_Wroom02), + (["reg_3v3"], Tps54202h), + (["drv", "vm_cap1", "cap"], AluminumCapacitor), ], instance_values=[ - (['refdes_prefix'], 'B'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'led=_GPIO9_STRAP', # force using the strapping / boot mode pin - 'vin_sense=3', # 4 as sent to fabrication before ADC2 removed from model, blue-wire to 3 - 'motor1=15', - 'motor2=14', - 'enca=13', - 'encb=10', - 'qwiic.sda=6', - 'qwiic.scl=5', - ]), - (['mcu', 'programming'], 'uart-auto'), - (['reg_3v3', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 9e6)), - (['drv', 'isen_res', 'res', 'footprint_spec'], 'Resistor_SMD:R_1206_3216Metric'), - (['drv', 'isen_res', 'res', 'require_basic_part'], False), - (['reg_3v3', 'power_path', 'in_cap', 'cap', 'voltage_rating_derating'], 1.0), + (["refdes_prefix"], "B"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "led=_GPIO9_STRAP", # force using the strapping / boot mode pin + "vin_sense=3", # 4 as sent to fabrication before ADC2 removed from model, blue-wire to 3 + "motor1=15", + "motor2=14", + "enca=13", + "encb=10", + "qwiic.sda=6", + "qwiic.scl=5", + ], + ), + (["mcu", "programming"], "uart-auto"), + (["reg_3v3", "power_path", "inductor", "manual_frequency_rating"], Range(0, 9e6)), + (["drv", "isen_res", "res", "footprint_spec"], "Resistor_SMD:R_1206_3216Metric"), + (["drv", "isen_res", "res", "require_basic_part"], False), + (["reg_3v3", "power_path", "in_cap", "cap", "voltage_rating_derating"], 1.0), # 15uH inductors are more common - (['reg_3v3', 'power_path', 'inductor', 'inductance'], Range.from_tolerance(15e-6, 0.2)) + (["reg_3v3", "power_path", "inductor", "inductance"], Range.from_tolerance(15e-6, 0.2)), ], class_refinements=[ (EspProgrammingHeader, EspProgrammingTc2030), (TestPoint, CompactKeystone5015), ], class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), - ] + (CompactKeystone5015, ["lcsc_part"], "C5199798"), + ], ) @@ -169,8 +181,8 @@ class MotorConnector(Block): def __init__(self) -> None: super().__init__() self.conn = self.Block(Picoblade(length=2)) - self.motor1 = self.Export(self.conn.pins.request('1').adapt_to(DigitalSink(current_draw=(0, 0.5)*Amp))) - self.motor2 = self.Export(self.conn.pins.request('2').adapt_to(DigitalSink(current_draw=(0, 0.5)*Amp))) + self.motor1 = self.Export(self.conn.pins.request("1").adapt_to(DigitalSink(current_draw=(0, 0.5) * Amp))) + self.motor2 = self.Export(self.conn.pins.request("2").adapt_to(DigitalSink(current_draw=(0, 0.5) * Amp))) class IotCurtainCrawler(JlcBoardTop): @@ -182,6 +194,7 @@ class IotCurtainCrawler(JlcBoardTop): Hall sensor is SOT-23 type, marked "6201" """ + @override def contents(self) -> None: super().contents() @@ -196,11 +209,11 @@ def contents(self) -> None: # POWER with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.gnd, [Common]), ) as imp: (self.fuse, self.ferrite, self.tp_vin), _ = self.chain( self.vin_raw, - self.Block(SeriesPowerFuse(trip_current=(300, 600)*mAmp)), + self.Block(SeriesPowerFuse(trip_current=(300, 600) * mAmp)), self.Block(SeriesPowerFerriteBead()), self.Block(VoltageTestPoint()), ) @@ -208,98 +221,103 @@ def contents(self) -> None: (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( self.vin, - imp.Block(VoltageRegulator(output_voltage=3.3*Volt(tol=0.05))), + imp.Block(VoltageRegulator(output_voltage=3.3 * Volt(tol=0.05))), self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), ) self.v3v3 = self.connect(self.reg_3v3.pwr_out) # 3V3 DOMAIN with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: self.mcu = imp.Block(IoController()) self.mcu.with_mixin(IoControllerWifi()) # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request('led')) + (self.ledr,), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request("led")) - (self.vin_sense, ), _ = self.chain( + (self.vin_sense,), _ = self.chain( self.vin, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('vin_sense') + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("vin_sense"), ) - (self.enca, ), _ = self.chain(imp.Block(Ah1806()), self.mcu.gpio.request('enca')) - (self.encb, ), _ = self.chain(imp.Block(Ah1806()), self.mcu.gpio.request('encb')) + (self.enca,), _ = self.chain(imp.Block(Ah1806()), self.mcu.gpio.request("enca")) + (self.encb,), _ = self.chain(imp.Block(Ah1806()), self.mcu.gpio.request("encb")) - self.i2c = self.mcu.i2c.request('i2c') + self.i2c = self.mcu.i2c.request("i2c") (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( - self.i2c, - imp.Block(I2cPullup()), imp.Block(I2cTestPoint('i2c'))) + self.i2c, imp.Block(I2cPullup()), imp.Block(I2cTestPoint("i2c")) + ) self.als = imp.Block(Bh1750()) self.connect(self.i2c, self.als.i2c) - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) # generic expansion - (self.qwiic, ), _ = self.chain(self.i2c, imp.Block(QwiicTarget())) + (self.qwiic,), _ = self.chain(self.i2c, imp.Block(QwiicTarget())) # 12V DOMAIN with self.implicit_connect( - ImplicitConnect(self.vin, [Power]), - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.vin, [Power]), + ImplicitConnect(self.gnd, [Common]), ) as imp: self.motor = self.Block(MotorConnector()) - self.drv = imp.Block(Drv8870(current_trip=150*mAmp(tol=0.1))) + self.drv = imp.Block(Drv8870(current_trip=150 * mAmp(tol=0.1))) self.connect(self.drv.vref, self.v3v3) - self.connect(self.mcu.gpio.request('motor1'), self.drv.in1) - self.connect(self.mcu.gpio.request('motor2'), self.drv.in2) + self.connect(self.mcu.gpio.request("motor1"), self.drv.in1) + self.connect(self.mcu.gpio.request("motor2"), self.drv.in2) self.connect(self.drv.out1, self.motor.motor1) self.connect(self.drv.out2, self.motor.motor2) - self._block_diagram_grouping = self.Metadata({ - 'pwr': 'pwr, pwr_out, fuse, tp_vin, reg_3v3, prot_3v3, vin_sense', - 'mcu': 'mcu, ledr, i2c_pull, als, qwiic', - 'app': 'motor, drv, enca, encb', - }) + self._block_diagram_grouping = self.Metadata( + { + "pwr": "pwr, pwr_out, fuse, tp_vin, reg_3v3, prot_3v3, vin_sense", + "mcu": "mcu, ledr, i2c_pull, als, qwiic", + "app": "motor, drv, enca, encb", + } + ) @override def refinements(self) -> Refinements: return super().refinements() + Refinements( instance_refinements=[ - (['mcu'], Esp32c3_Wroom02), - (['reg_3v3'], Tps54202h), - (['drv', 'vm_cap1', 'cap'], AluminumCapacitor), + (["mcu"], Esp32c3_Wroom02), + (["reg_3v3"], Tps54202h), + (["drv", "vm_cap1", "cap"], AluminumCapacitor), ], instance_values=[ - (['refdes_prefix'], 'R'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'led=_GPIO9_STRAP', # force using the strapping / boot mode pin - 'vin_sense=17', # 4 as sent to fabrication before ADC2 removed from model, blue-wire to 17 - 'motor1=14', - 'motor2=15', - 'enca=13', - 'encb=10', - 'i2c.sda=5', - 'i2c.scl=6', - 'sw=3', - ]), - (['mcu', 'programming'], 'uart-auto'), - (['reg_3v3', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 9e6)), - (['drv', 'isen_res', 'res', 'footprint_spec'], 'Resistor_SMD:R_1206_3216Metric'), - (['drv', 'isen_res', 'res', 'require_basic_part'], False), - (['reg_3v3', 'power_path', 'in_cap', 'cap', 'voltage_rating_derating'], 1.0), + (["refdes_prefix"], "R"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "led=_GPIO9_STRAP", # force using the strapping / boot mode pin + "vin_sense=17", # 4 as sent to fabrication before ADC2 removed from model, blue-wire to 17 + "motor1=14", + "motor2=15", + "enca=13", + "encb=10", + "i2c.sda=5", + "i2c.scl=6", + "sw=3", + ], + ), + (["mcu", "programming"], "uart-auto"), + (["reg_3v3", "power_path", "inductor", "manual_frequency_rating"], Range(0, 9e6)), + (["drv", "isen_res", "res", "footprint_spec"], "Resistor_SMD:R_1206_3216Metric"), + (["drv", "isen_res", "res", "require_basic_part"], False), + (["reg_3v3", "power_path", "in_cap", "cap", "voltage_rating_derating"], 1.0), # 15uH inductors are more common - (['reg_3v3', 'power_path', 'inductor', 'inductance'], Range.from_tolerance(15e-6, 0.2)) + (["reg_3v3", "power_path", "inductor", "inductance"], Range.from_tolerance(15e-6, 0.2)), ], class_refinements=[ (EspProgrammingHeader, EspProgrammingTc2030), (TestPoint, CompactKeystone5015), ], class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), - ] + (CompactKeystone5015, ["lcsc_part"], "C5199798"), + ], ) diff --git a/examples/test_iot_display.py b/examples/test_iot_display.py index df04d6ef5..61c056685 100644 --- a/examples/test_iot_display.py +++ b/examples/test_iot_display.py @@ -6,198 +6,212 @@ class PmosHighSideSwitch(PowerSwitch): - """Single PMOS switch. - TODO: this should be generated by HighSideSwitch if the voltage is low enough, - but making it smart depends on the voltage which is not available until the current is solved for - (need some sort of phased generator support)""" - def __init__(self, frequency: RangeLike = RangeExpr.ZERO, max_rds: FloatLike = 1*Ohm) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - - self.control = self.Port(DigitalSink.empty(), [Input]) - self.output = self.Port(VoltageSource.empty(), [Output]) - - self.frequency = self.ArgParameter(frequency) - self.max_rds = self.ArgParameter(max_rds) - - @override - def contents(self) -> None: - super().contents() - - self.drv = self.Block(SwitchFet.PFet( - drain_voltage=self.pwr.link().voltage, - drain_current=self.output.link().current_drawn, - gate_voltage=self.control.link().voltage - self.pwr.link().voltage, # TODO needs to be diff from pwr.voltage - rds_on=(0, self.max_rds), - frequency=self.frequency, - drive_current=self.control.link().current_limits # TODO this is kind of a max drive current - )) - - self.connect(self.pwr, self.drv.source.adapt_to(VoltageSink( - current_draw=self.output.link().current_drawn - ))) - self.connect(self.output, self.drv.drain.adapt_to(VoltageSource( - voltage_out=self.pwr.link().voltage, - current_limits=self.drv.actual_drain_current_rating, - ))) - self.connect(self.control, self.drv.gate.adapt_to(DigitalSink())) + """Single PMOS switch. + TODO: this should be generated by HighSideSwitch if the voltage is low enough, + but making it smart depends on the voltage which is not available until the current is solved for + (need some sort of phased generator support)""" + + def __init__(self, frequency: RangeLike = RangeExpr.ZERO, max_rds: FloatLike = 1 * Ohm) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + + self.control = self.Port(DigitalSink.empty(), [Input]) + self.output = self.Port(VoltageSource.empty(), [Output]) + + self.frequency = self.ArgParameter(frequency) + self.max_rds = self.ArgParameter(max_rds) + + @override + def contents(self) -> None: + super().contents() + + self.drv = self.Block( + SwitchFet.PFet( + drain_voltage=self.pwr.link().voltage, + drain_current=self.output.link().current_drawn, + gate_voltage=self.control.link().voltage + - self.pwr.link().voltage, # TODO needs to be diff from pwr.voltage + rds_on=(0, self.max_rds), + frequency=self.frequency, + drive_current=self.control.link().current_limits, # TODO this is kind of a max drive current + ) + ) + + self.connect(self.pwr, self.drv.source.adapt_to(VoltageSink(current_draw=self.output.link().current_drawn))) + self.connect( + self.output, + self.drv.drain.adapt_to( + VoltageSource( + voltage_out=self.pwr.link().voltage, + current_limits=self.drv.actual_drain_current_rating, + ) + ), + ) + self.connect(self.control, self.drv.gate.adapt_to(DigitalSink())) class IotDisplay(JlcBoardTop): - """Battery-powered IoT e-paper display with deep sleep. - """ - @override - def contents(self) -> None: - super().contents() - - BATTERY_VOLTAGE = (4*1.15, 6*1.6)*Volt - - self.usb = self.Block(UsbCReceptacle(current_limits=(0, 3)*Amp)) - self.batt = self.Block(LipoConnector(voltage=BATTERY_VOLTAGE, actual_voltage=BATTERY_VOLTAGE)) # 2-6 AA - - self.gnd = self.connect(self.usb.gnd, self.batt.gnd) - - self.tp_pwr = self.Block(VoltageTestPoint("batt")).connected(self.batt.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.vbat_prot, self.reg_3v3, self.tp_3v3), _ = self.chain( - self.batt.pwr, - imp.Block(PmosReverseProtection()), - imp.Block(BuckConverter(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()) - ) - self.vbat = self.connect(self.vbat_prot.pwr_out) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - self.vbat_sense_gate = imp.Block(HighSideSwitch()) - self.connect(self.vbat_sense_gate.pwr, self.vbat) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerWifi()) - - # need to name the USB chain so the USB net has the _N and _P postfix for differential traces - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) - - # DEBUGGING UI ELEMENTS - (self.ledr, ), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request('ledr')) - (self.ledg, ), _ = self.chain(imp.Block(IndicatorLed(Led.Yellow)), self.mcu.gpio.request('ledg')) - (self.ledb, ), _ = self.chain(imp.Block(IndicatorLed(Led.White)), self.mcu.gpio.request('ledb')) - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request(f'sw')) - - # SENSING - self.connect(self.vbat_sense_gate.control, self.mcu.gpio.request('vbat_sense_gate')) - (self.vbat_sense, ), _ = self.chain( - self.vbat_sense_gate.output, - imp.Block(VoltageSenseDivider(full_scale_voltage=(0, 2.9)*Volt, impedance=(10, 100)*kOhm)), - self.mcu.adc.request('vbat_sense')) - - mcu_touch = self.mcu.with_mixin(IoControllerTouchDriver()) - (self.touch_duck, ), _ = self.chain( - mcu_touch.touch.request('touch_duck'), - imp.Block(FootprintToucbPad('edg:Symbol_DucklingSolid')) - ) - (self.touch_lemur, ), _ = self.chain( - mcu_touch.touch.request('touch_lemur'), - imp.Block(FootprintToucbPad('edg:Symbol_LemurSolid')) - ) - - gate_model = PmosHighSideSwitch(max_rds=0.1*Ohm) - (self.epd_gate, ), _ = self.chain(self.mcu.gpio.request('epd_gate'), imp.Block(gate_model)) - (self.mem_gate, ), _ = self.chain(self.mcu.gpio.request('mem_gate'), imp.Block(gate_model)) - - # DISPLAY POWER DOMAIN - with self.implicit_connect( + """Battery-powered IoT e-paper display with deep sleep.""" + + @override + def contents(self) -> None: + super().contents() + + BATTERY_VOLTAGE = (4 * 1.15, 6 * 1.6) * Volt + + self.usb = self.Block(UsbCReceptacle(current_limits=(0, 3) * Amp)) + self.batt = self.Block(LipoConnector(voltage=BATTERY_VOLTAGE, actual_voltage=BATTERY_VOLTAGE)) # 2-6 AA + + self.gnd = self.connect(self.usb.gnd, self.batt.gnd) + + self.tp_pwr = self.Block(VoltageTestPoint("batt")).connected(self.batt.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.vbat_prot, self.reg_3v3, self.tp_3v3), _ = self.chain( + self.batt.pwr, + imp.Block(PmosReverseProtection()), + imp.Block(BuckConverter(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + ) + self.vbat = self.connect(self.vbat_prot.pwr_out) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + self.vbat_sense_gate = imp.Block(HighSideSwitch()) + self.connect(self.vbat_sense_gate.pwr, self.vbat) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerWifi()) + + # need to name the USB chain so the USB net has the _N and _P postfix for differential traces + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + # DEBUGGING UI ELEMENTS + (self.ledr,), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request("ledr")) + (self.ledg,), _ = self.chain(imp.Block(IndicatorLed(Led.Yellow)), self.mcu.gpio.request("ledg")) + (self.ledb,), _ = self.chain(imp.Block(IndicatorLed(Led.White)), self.mcu.gpio.request("ledb")) + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request(f"sw")) + + # SENSING + self.connect(self.vbat_sense_gate.control, self.mcu.gpio.request("vbat_sense_gate")) + (self.vbat_sense,), _ = self.chain( + self.vbat_sense_gate.output, + imp.Block(VoltageSenseDivider(full_scale_voltage=(0, 2.9) * Volt, impedance=(10, 100) * kOhm)), + self.mcu.adc.request("vbat_sense"), + ) + + mcu_touch = self.mcu.with_mixin(IoControllerTouchDriver()) + (self.touch_duck,), _ = self.chain( + mcu_touch.touch.request("touch_duck"), imp.Block(FootprintToucbPad("edg:Symbol_DucklingSolid")) + ) + (self.touch_lemur,), _ = self.chain( + mcu_touch.touch.request("touch_lemur"), imp.Block(FootprintToucbPad("edg:Symbol_LemurSolid")) + ) + + gate_model = PmosHighSideSwitch(max_rds=0.1 * Ohm) + (self.epd_gate,), _ = self.chain(self.mcu.gpio.request("epd_gate"), imp.Block(gate_model)) + (self.mem_gate,), _ = self.chain(self.mcu.gpio.request("mem_gate"), imp.Block(gate_model)) + + # DISPLAY POWER DOMAIN + with self.implicit_connect( ImplicitConnect(self.epd_gate.output, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.epd = imp.Block(Waveshare_Epd()) - (self.tp_epd, ), _ = self.chain(self.mcu.spi.request('epd'), imp.Block(SpiTestPoint('epd')), self.epd.spi) - (self.tp_erst, ), _ = self.chain(self.mcu.gpio.request('epd_rst'), imp.Block(DigitalTestPoint('rst')), self.epd.reset) - (self.tp_dc, ), _ = self.chain(self.mcu.gpio.request('epd_dc'), imp.Block(DigitalTestPoint('dc')), self.epd.dc) - (self.tp_epd_cs, ), _ = self.chain(self.mcu.gpio.request('epd_cs'), imp.Block(DigitalTestPoint('cs')), self.epd.cs) - (self.tp_busy, ), _ = self.chain(self.mcu.gpio.request('epd_busy'), imp.Block(DigitalTestPoint('bsy')), self.epd.busy) - - # MEMORY POWER DOMAIN - with self.implicit_connect( + ) as imp: + self.epd = imp.Block(Waveshare_Epd()) + (self.tp_epd,), _ = self.chain(self.mcu.spi.request("epd"), imp.Block(SpiTestPoint("epd")), self.epd.spi) + (self.tp_erst,), _ = self.chain( + self.mcu.gpio.request("epd_rst"), imp.Block(DigitalTestPoint("rst")), self.epd.reset + ) + (self.tp_dc,), _ = self.chain( + self.mcu.gpio.request("epd_dc"), imp.Block(DigitalTestPoint("dc")), self.epd.dc + ) + (self.tp_epd_cs,), _ = self.chain( + self.mcu.gpio.request("epd_cs"), imp.Block(DigitalTestPoint("cs")), self.epd.cs + ) + (self.tp_busy,), _ = self.chain( + self.mcu.gpio.request("epd_busy"), imp.Block(DigitalTestPoint("bsy")), self.epd.busy + ) + + # MEMORY POWER DOMAIN + with self.implicit_connect( ImplicitConnect(self.mem_gate.output, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mem_spi = self.mcu.spi.request('sd') - self.sd = imp.Block(SdCard()) - self.flash = imp.Block(W25q(Range.from_lower(16*1024*1024))) # at least 16Mbit - (self.tp_sd, ), _ = self.chain(self.mem_spi, imp.Block(SpiTestPoint('sd'))) - self.connect(self.mem_spi, self.sd.spi, self.flash.spi) - (self.tp_sd_cs, ), _ = self.chain(self.mcu.gpio.request('sd_cs'), imp.Block(DigitalTestPoint('sd_cs')), self.sd.cs) - self.connect(self.mcu.gpio.request('fl_cs'), self.flash.cs) # no test point, clip the SOIC - - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['reg_3v3'], Tps54202h), - (['batt', 'conn'], JstPhKHorizontal), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - # note: for ESP32-S3 compatibility: IO35/36/37 (pins 28-30) are used by PSRAM - # note: for ESP32-C6 compatibility: pin 34 (22 on dedicated -C6 pattern) is NC - 'ledr=39', - 'ledg=38', - 'ledb=4', - 'sw=5', - 'epd_dc=31', - 'epd_cs=32', - 'epd.sck=33', - 'epd.mosi=35', - 'epd.miso=NC', - 'epd_rst=8', - 'epd_busy=9', - 'epd_gate=10', - 'touch_duck=GPIO13', - 'touch_lemur=GPIO14', - - 'vbat_sense=7', - 'vbat_sense_gate=6', - - 'sd.miso=15', - 'sd.sck=17', - 'sd.mosi=18', - 'sd_cs=19', - 'fl_cs=20', - 'mem_gate=23', - ]), - (['mcu', 'programming'], 'uart-auto-button'), - - (['reg_3v3', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 10e6)), - (['reg_3v3', 'fb', 'impedance'], Range(20000.0, 100000.0)), - (['epd', 'boost', 'sense', 'require_basic_part'], False), # 3R is not a basic part - (['epd', 'boost', 'inductor', 'part'], "CBC3225T680KR") # automated selection OOS - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (TestPoint, CompactKeystone5015), - (Fpc050Bottom, Fpc050BottomFlip), # top-contact so board is side-by-side with display - (SdCard, Molex1040310811), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - ] - ) + ) as imp: + self.mem_spi = self.mcu.spi.request("sd") + self.sd = imp.Block(SdCard()) + self.flash = imp.Block(W25q(Range.from_lower(16 * 1024 * 1024))) # at least 16Mbit + (self.tp_sd,), _ = self.chain(self.mem_spi, imp.Block(SpiTestPoint("sd"))) + self.connect(self.mem_spi, self.sd.spi, self.flash.spi) + (self.tp_sd_cs,), _ = self.chain( + self.mcu.gpio.request("sd_cs"), imp.Block(DigitalTestPoint("sd_cs")), self.sd.cs + ) + self.connect(self.mcu.gpio.request("fl_cs"), self.flash.cs) # no test point, clip the SOIC + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["reg_3v3"], Tps54202h), + (["batt", "conn"], JstPhKHorizontal), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + # note: for ESP32-S3 compatibility: IO35/36/37 (pins 28-30) are used by PSRAM + # note: for ESP32-C6 compatibility: pin 34 (22 on dedicated -C6 pattern) is NC + "ledr=39", + "ledg=38", + "ledb=4", + "sw=5", + "epd_dc=31", + "epd_cs=32", + "epd.sck=33", + "epd.mosi=35", + "epd.miso=NC", + "epd_rst=8", + "epd_busy=9", + "epd_gate=10", + "touch_duck=GPIO13", + "touch_lemur=GPIO14", + "vbat_sense=7", + "vbat_sense_gate=6", + "sd.miso=15", + "sd.sck=17", + "sd.mosi=18", + "sd_cs=19", + "fl_cs=20", + "mem_gate=23", + ], + ), + (["mcu", "programming"], "uart-auto-button"), + (["reg_3v3", "power_path", "inductor", "manual_frequency_rating"], Range(0, 10e6)), + (["reg_3v3", "fb", "impedance"], Range(20000.0, 100000.0)), + (["epd", "boost", "sense", "require_basic_part"], False), # 3R is not a basic part + (["epd", "boost", "inductor", "part"], "CBC3225T680KR"), # automated selection OOS + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (TestPoint, CompactKeystone5015), + (Fpc050Bottom, Fpc050BottomFlip), # top-contact so board is side-by-side with display + (SdCard, Molex1040310811), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + ], + ) class IotDisplayTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(IotDisplay) + def test_design(self) -> None: + compile_board_inplace(IotDisplay) diff --git a/examples/test_iot_fan.py b/examples/test_iot_fan.py index d1660954a..13cf4a4da 100644 --- a/examples/test_iot_fan.py +++ b/examples/test_iot_fan.py @@ -6,146 +6,146 @@ class IotFan(JlcBoardTop): - """IoT fan controller with a 12v barrel jack input and a CPU fan connector. - """ - @override - def contents(self) -> None: - super().contents() - - RING_LEDS = 18 # number of RGBs for the ring indicator - - self.pwr = self.Block(PowerBarrelJack(voltage_out=12*Volt(tol=0.05), current_limits=(0, 5)*Amp)) - - self.v12 = self.connect(self.pwr.pwr) - self.gnd = self.connect(self.pwr.gnd) - - self.tp_pwr = self.Block(VoltageTestPoint()).connected(self.pwr.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.pwr.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_5v, self.tp_5v, self.prot_5v), _ = self.chain( - self.v12, - imp.Block(VoltageRegulator(output_voltage=4*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(5.5, 6.8)*Volt)) - ) - self.v5 = self.connect(self.reg_5v.pwr_out) - - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.v5, - imp.Block(VoltageRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerWifi()) - - # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request('led')) - - self.enc = imp.Block(DigitalRotaryEncoder()) - self.connect(self.enc.a, self.mcu.gpio.request('enc_a')) - self.connect(self.enc.b, self.mcu.gpio.request('enc_b')) - self.connect(self.enc.with_mixin(DigitalRotaryEncoderSwitch()).sw, self.mcu.gpio.request('enc_sw')) - - (self.v12_sense, ), _ = self.chain( - self.v12, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('v12_sense') - ) - - # 5V DOMAIN - with self.implicit_connect( + """IoT fan controller with a 12v barrel jack input and a CPU fan connector.""" + + @override + def contents(self) -> None: + super().contents() + + RING_LEDS = 18 # number of RGBs for the ring indicator + + self.pwr = self.Block(PowerBarrelJack(voltage_out=12 * Volt(tol=0.05), current_limits=(0, 5) * Amp)) + + self.v12 = self.connect(self.pwr.pwr) + self.gnd = self.connect(self.pwr.gnd) + + self.tp_pwr = self.Block(VoltageTestPoint()).connected(self.pwr.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.pwr.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_5v, self.tp_5v, self.prot_5v), _ = self.chain( + self.v12, + imp.Block(VoltageRegulator(output_voltage=4 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(5.5, 6.8) * Volt)), + ) + self.v5 = self.connect(self.reg_5v.pwr_out) + + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.v5, + imp.Block(VoltageRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerWifi()) + + # debugging LEDs + (self.ledr,), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request("led")) + + self.enc = imp.Block(DigitalRotaryEncoder()) + self.connect(self.enc.a, self.mcu.gpio.request("enc_a")) + self.connect(self.enc.b, self.mcu.gpio.request("enc_b")) + self.connect(self.enc.with_mixin(DigitalRotaryEncoderSwitch()).sw, self.mcu.gpio.request("enc_sw")) + + (self.v12_sense,), _ = self.chain( + self.v12, + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("v12_sense"), + ) + + # 5V DOMAIN + with self.implicit_connect( ImplicitConnect(self.v5, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.rgb_ring, ), _ = self.chain( - self.mcu.gpio.request('rgb'), - imp.Block(NeopixelArray(RING_LEDS))) - - # 12V DOMAIN - self.fan = ElementDict[CpuFanConnector]() - self.fan_drv = ElementDict[HighSideSwitch]() - self.fan_sense = ElementDict[OpenDrainDriver]() - self.fan_ctl = ElementDict[OpenDrainDriver]() - - with self.implicit_connect( + ) as imp: + (self.rgb_ring,), _ = self.chain(self.mcu.gpio.request("rgb"), imp.Block(NeopixelArray(RING_LEDS))) + + # 12V DOMAIN + self.fan = ElementDict[CpuFanConnector]() + self.fan_drv = ElementDict[HighSideSwitch]() + self.fan_sense = ElementDict[OpenDrainDriver]() + self.fan_ctl = ElementDict[OpenDrainDriver]() + + with self.implicit_connect( ImplicitConnect(self.v12, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - for i in range(2): - fan = self.fan[i] = self.Block(CpuFanConnector()) - fan_drv = self.fan_drv[i] = imp.Block(HighSideSwitch(pull_resistance=4.7*kOhm(tol=0.05), max_rds=0.3*Ohm)) - self.connect(fan.pwr, fan_drv.output) - self.connect(fan.gnd, self.gnd) - self.connect(self.mcu.gpio.request(f'fan_drv_{i}'), fan_drv.control) - - self.connect(fan.sense, self.mcu.gpio.request(f'fan_sense_{i}')) - (self.fan_ctl[i], ), _ = self.chain( - self.mcu.gpio.request(f'fan_ctl_{i}'), - imp.Block(OpenDrainDriver()), - fan.with_mixin(CpuFanPwmControl()).control + ) as imp: + for i in range(2): + fan = self.fan[i] = self.Block(CpuFanConnector()) + fan_drv = self.fan_drv[i] = imp.Block( + HighSideSwitch(pull_resistance=4.7 * kOhm(tol=0.05), max_rds=0.3 * Ohm) + ) + self.connect(fan.pwr, fan_drv.output) + self.connect(fan.gnd, self.gnd) + self.connect(self.mcu.gpio.request(f"fan_drv_{i}"), fan_drv.control) + + self.connect(fan.sense, self.mcu.gpio.request(f"fan_sense_{i}")) + (self.fan_ctl[i],), _ = self.chain( + self.mcu.gpio.request(f"fan_ctl_{i}"), + imp.Block(OpenDrainDriver()), + fan.with_mixin(CpuFanPwmControl()).control, + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32c3), + (["reg_5v"], Tps54202h), + (["reg_3v3"], Ap7215), + ], + instance_values=[ + (["refdes_prefix"], "F"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "v12_sense=4", + "rgb=_GPIO2_STRAP_EXT_PU", # force using the strapping pin, since we're out of IOs + "led=_GPIO9_STRAP", # force using the strapping / boot mode pin + "fan_drv_0=5", + "fan_ctl_0=8", + "fan_sense_0=9", + "fan_drv_1=10", + "fan_ctl_1=13", + "fan_sense_1=12", + "enc_sw=25", + "enc_b=16", + "enc_a=26", + ], + ), + (["mcu", "programming"], "uart-auto"), + (["reg_5v", "power_path", "inductor", "manual_frequency_rating"], Range(0, 9e6)), + (["fan_drv[0]", "drv", "footprint_spec"], "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm"), + (["fan_drv[0]", "drv", "part"], "AO4407A"), # default DMG4407 is out of stock + (["fan_drv[1]", "drv", "footprint_spec"], ParamValue(["fan_drv[0]", "drv", "footprint_spec"])), + (["fan_drv[1]", "drv", "part"], ParamValue(["fan_drv[0]", "drv", "part"])), + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (PowerBarrelJack, Pj_036ah), + (Neopixel, Sk6805_Ec15), + (TestPoint, CompactKeystone5015), + (TagConnect, TagConnectNonLegged), + ], + class_values=[ + (Esp32c3, ["not_recommended"], True), + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + (DiscreteRfWarning, ["discrete_rf_warning"], False), + ], ) - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32c3), - (['reg_5v'], Tps54202h), - (['reg_3v3'], Ap7215), - ], - instance_values=[ - (['refdes_prefix'], 'F'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'v12_sense=4', - 'rgb=_GPIO2_STRAP_EXT_PU', # force using the strapping pin, since we're out of IOs - 'led=_GPIO9_STRAP', # force using the strapping / boot mode pin - - 'fan_drv_0=5', - 'fan_ctl_0=8', - 'fan_sense_0=9', - - 'fan_drv_1=10', - 'fan_ctl_1=13', - 'fan_sense_1=12', - - 'enc_sw=25', - 'enc_b=16', - 'enc_a=26', - ]), - (['mcu', 'programming'], 'uart-auto'), - (['reg_5v', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 9e6)), - (['fan_drv[0]', 'drv', 'footprint_spec'], "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm"), - (['fan_drv[0]', 'drv', 'part'], "AO4407A"), # default DMG4407 is out of stock - (['fan_drv[1]', 'drv', 'footprint_spec'], ParamValue(['fan_drv[0]', 'drv', 'footprint_spec'])), - (['fan_drv[1]', 'drv', 'part'], ParamValue(['fan_drv[0]', 'drv', 'part'])), - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (PowerBarrelJack, Pj_036ah), - (Neopixel, Sk6805_Ec15), - (TestPoint, CompactKeystone5015), - (TagConnect, TagConnectNonLegged), - ], - class_values=[ - (Esp32c3, ['not_recommended'], True), - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - (DiscreteRfWarning, ['discrete_rf_warning'], False), - ] - ) - class IotFanTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(IotFan) + def test_design(self) -> None: + compile_board_inplace(IotFan) diff --git a/examples/test_iot_iron.py b/examples/test_iot_iron.py index a8361ed9a..eedb4b864 100644 --- a/examples/test_iot_iron.py +++ b/examples/test_iot_iron.py @@ -6,259 +6,261 @@ class IronConnector(Connector, Block): - """See main design for details about pinning and compatibility. - This assumes a common ground with heater+ and thermocouple+. - TODO: support series heater and thermocouple, requires additional protection circuits on amps - TODO: optional generation for isense_res, if not connected - """ - def __init__(self, *, isense_resistance: RangeLike = 22*mOhm(tol=0.05), current_draw: RangeLike=(0, 3.25)*Amp): - super().__init__() - self.conn = self.Block(PinHeader254(3)) - - self.gnd = self.Port(Ground.empty(), [Common]) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSink( - current_draw=current_draw - ))) - self.thermocouple = self.Export(self.conn.pins.request('3').adapt_to(AnalogSource( - voltage_out=self.gnd.link().voltage + (0, 14.3)*mVolt, - signal_out=self.gnd.link().voltage + (0, 14.3)*mVolt # up to ~350 C - )), optional=True) - - self.isense_res = self.Block(CurrentSenseResistor(resistance=isense_resistance, sense_in_reqd=False)) - self.isense = self.Export(self.isense_res.sense_out) - self.connect(self.conn.pins.request('1').adapt_to(VoltageSink(current_draw=current_draw)), - self.isense_res.pwr_out) - self.connect(self.gnd.as_voltage_source(), self.isense_res.pwr_in) + """See main design for details about pinning and compatibility. + This assumes a common ground with heater+ and thermocouple+. + TODO: support series heater and thermocouple, requires additional protection circuits on amps + TODO: optional generation for isense_res, if not connected + """ + + def __init__( + self, *, isense_resistance: RangeLike = 22 * mOhm(tol=0.05), current_draw: RangeLike = (0, 3.25) * Amp + ): + super().__init__() + self.conn = self.Block(PinHeader254(3)) + + self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Export(self.conn.pins.request("2").adapt_to(VoltageSink(current_draw=current_draw))) + self.thermocouple = self.Export( + self.conn.pins.request("3").adapt_to( + AnalogSource( + voltage_out=self.gnd.link().voltage + (0, 14.3) * mVolt, + signal_out=self.gnd.link().voltage + (0, 14.3) * mVolt, # up to ~350 C + ) + ), + optional=True, + ) + + self.isense_res = self.Block(CurrentSenseResistor(resistance=isense_resistance, sense_in_reqd=False)) + self.isense = self.Export(self.isense_res.sense_out) + self.connect( + self.conn.pins.request("1").adapt_to(VoltageSink(current_draw=current_draw)), self.isense_res.pwr_out + ) + self.connect(self.gnd.as_voltage_source(), self.isense_res.pwr_in) class IotIron(JlcBoardTop): - """IoT soldering iron controller (ceramic heater type, not RF heating type) with USB-PD in, - buck converter for maximum compatibility and reduced EMI, and builtin UI components (in addition - to wireless connectivity). - - Inspired by https://github.com/AxxAxx/AxxSolder/tree/main, see repo README for links on connector pinning. - """ - @override - def contents(self) -> None: - super().contents() - - # assume minimum power input of 12v from PD, you probably don't want a 5v USB 15W soldering iron - self.usb = self.Block(UsbCReceptacle(voltage_out=(12, 20)*Volt, current_limits=(0, 5)*Amp)) - - self.vusb = self.connect(self.usb.pwr) - self.gnd = self.connect(self.usb.gnd) - - self.tp_pwr = self.Block(VoltageTestPoint()).connected(self.usb.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.vusb, - imp.Block(BuckConverter(output_voltage=3.3*Volt(tol=0.05), - input_ripple_limit=100*mVolt)), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # set gate driver at 9v to allow power from USB-PD 9v - (self.reg_gate, self.tp_gate), _ = self.chain( - self.vusb, - imp.Block(VoltageRegulator(output_voltage=12*Volt(tol=0.06))), - self.Block(VoltageTestPoint()) - ) - self.vgate = self.connect(self.reg_gate.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerWifi()) - self.i2c = self.mcu.i2c.request('i2c') - (self.i2c_pull, ), _ = self.chain(self.i2c, imp.Block(I2cPullup())) - - # power input - self.pd = imp.Block(Fusb302b()) - self.connect(self.usb.pwr, self.pd.vbus) - self.connect(self.usb.cc, self.pd.cc) - self.connect(self.mcu.gpio.request('pd_int'), self.pd.int) - self.connect(self.i2c, self.pd.i2c) - - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) - - (self.vusb_sense, ), _ = self.chain( - self.vusb, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('vusb_sense') - ) - - # sensing - cold junction compensation - (self.temp, ), _ = self.chain(self.i2c, imp.Block(Hdc1080())) - - # onboard user interface - self.enc = imp.Block(DigitalRotaryEncoder()) - self.connect(self.enc.a, self.mcu.gpio.request('enc_a')) - self.connect(self.enc.b, self.mcu.gpio.request('enc_b')) - self.connect(self.enc.with_mixin(DigitalRotaryEncoderSwitch()).sw, self.mcu.gpio.request('enc_sw')) - - self.oled = imp.Block(Er_Oled_096_1_1()) - self.connect(self.i2c, self.oled.i2c) - self.connect(self.mcu.gpio.request('oled_reset'), self.oled.reset) - - (self.spk_drv, self.spk), _ = self.chain( - self.mcu.with_mixin(IoControllerI2s()).i2s.request('spk'), - imp.Block(Max98357a()), - self.Block(Speaker()) - ) - - # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request('led')) - - - # IRON POWER SUPPLY - with self.implicit_connect( + """IoT soldering iron controller (ceramic heater type, not RF heating type) with USB-PD in, + buck converter for maximum compatibility and reduced EMI, and builtin UI components (in addition + to wireless connectivity). + + Inspired by https://github.com/AxxAxx/AxxSolder/tree/main, see repo README for links on connector pinning. + """ + + @override + def contents(self) -> None: + super().contents() + + # assume minimum power input of 12v from PD, you probably don't want a 5v USB 15W soldering iron + self.usb = self.Block(UsbCReceptacle(voltage_out=(12, 20) * Volt, current_limits=(0, 5) * Amp)) + + self.vusb = self.connect(self.usb.pwr) + self.gnd = self.connect(self.usb.gnd) + + self.tp_pwr = self.Block(VoltageTestPoint()).connected(self.usb.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.vusb, + imp.Block(BuckConverter(output_voltage=3.3 * Volt(tol=0.05), input_ripple_limit=100 * mVolt)), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # set gate driver at 9v to allow power from USB-PD 9v + (self.reg_gate, self.tp_gate), _ = self.chain( + self.vusb, + imp.Block(VoltageRegulator(output_voltage=12 * Volt(tol=0.06))), + self.Block(VoltageTestPoint()), + ) + self.vgate = self.connect(self.reg_gate.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerWifi()) + self.i2c = self.mcu.i2c.request("i2c") + (self.i2c_pull,), _ = self.chain(self.i2c, imp.Block(I2cPullup())) + + # power input + self.pd = imp.Block(Fusb302b()) + self.connect(self.usb.pwr, self.pd.vbus) + self.connect(self.usb.cc, self.pd.cc) + self.connect(self.mcu.gpio.request("pd_int"), self.pd.int) + self.connect(self.i2c, self.pd.i2c) + + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + (self.vusb_sense,), _ = self.chain( + self.vusb, + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("vusb_sense"), + ) + + # sensing - cold junction compensation + (self.temp,), _ = self.chain(self.i2c, imp.Block(Hdc1080())) + + # onboard user interface + self.enc = imp.Block(DigitalRotaryEncoder()) + self.connect(self.enc.a, self.mcu.gpio.request("enc_a")) + self.connect(self.enc.b, self.mcu.gpio.request("enc_b")) + self.connect(self.enc.with_mixin(DigitalRotaryEncoderSwitch()).sw, self.mcu.gpio.request("enc_sw")) + + self.oled = imp.Block(Er_Oled_096_1_1()) + self.connect(self.i2c, self.oled.i2c) + self.connect(self.mcu.gpio.request("oled_reset"), self.oled.reset) + + (self.spk_drv, self.spk), _ = self.chain( + self.mcu.with_mixin(IoControllerI2s()).i2s.request("spk"), imp.Block(Max98357a()), self.Block(Speaker()) + ) + + # debugging LEDs + (self.ledr,), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request("led")) + + # IRON POWER SUPPLY + with self.implicit_connect( ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.conv_force, self.conv, self.tp_conv), _ = self.chain( - self.vusb, - imp.Block(ForcedVoltage(20*Volt(tol=0))), - # want a high output ripple limit so the converter turns off fast to read the thermocouple - imp.Block(CustomSyncBuckConverterIndependent(output_voltage=(5, 5) * Volt, frequency=200 * kHertz(tol=0), - input_ripple_limit=1*Volt, - output_ripple_limit=0.25*Volt)), - self.Block(VoltageTestPoint()) - ) - self.conv_out = self.connect(self.conv.pwr_out) - self.connect(self.conv.pwr_logic, self.vgate) - pull_model = PulldownResistor(10*kOhm(tol=0.05)) - rc_model = DigitalLowPassRc(150*Ohm(tol=0.05), 7*MHertz(tol=0.2)) - (self.low_pull, self.low_rc), _ = self.chain(self.mcu.gpio.request('pwm_low'), - imp.Block(pull_model), - imp.Block(rc_model), - self.conv.pwm_low) - (self.high_pull, self.high_rc), _ = self.chain(self.mcu.gpio.request('pwm_high'), - imp.Block(pull_model), - imp.Block(rc_model), - self.conv.pwm_high) - self.tp_pwm_l = self.Block(DigitalTestPoint()).connected(self.conv.pwm_low) - self.tp_pwm_h = self.Block(DigitalTestPoint()).connected(self.conv.pwm_high) - - mcu_touch = self.mcu.with_mixin(IoControllerTouchDriver()) - (self.touch_sink, ), _ = self.chain( - mcu_touch.touch.request('touch'), - imp.Block(FootprintToucbPad('edg:Symbol_DucklingSolid')) - ) - - self.iron = imp.Block(IronConnector()) - self.connect(self.conv.pwr_out, self.iron.pwr) - - # IRON SENSE AMPS - 3v3 DOMAIN - with self.implicit_connect( + ) as imp: + (self.conv_force, self.conv, self.tp_conv), _ = self.chain( + self.vusb, + imp.Block(ForcedVoltage(20 * Volt(tol=0))), + # want a high output ripple limit so the converter turns off fast to read the thermocouple + imp.Block( + CustomSyncBuckConverterIndependent( + output_voltage=(5, 5) * Volt, + frequency=200 * kHertz(tol=0), + input_ripple_limit=1 * Volt, + output_ripple_limit=0.25 * Volt, + ) + ), + self.Block(VoltageTestPoint()), + ) + self.conv_out = self.connect(self.conv.pwr_out) + self.connect(self.conv.pwr_logic, self.vgate) + pull_model = PulldownResistor(10 * kOhm(tol=0.05)) + rc_model = DigitalLowPassRc(150 * Ohm(tol=0.05), 7 * MHertz(tol=0.2)) + (self.low_pull, self.low_rc), _ = self.chain( + self.mcu.gpio.request("pwm_low"), imp.Block(pull_model), imp.Block(rc_model), self.conv.pwm_low + ) + (self.high_pull, self.high_rc), _ = self.chain( + self.mcu.gpio.request("pwm_high"), imp.Block(pull_model), imp.Block(rc_model), self.conv.pwm_high + ) + self.tp_pwm_l = self.Block(DigitalTestPoint()).connected(self.conv.pwm_low) + self.tp_pwm_h = self.Block(DigitalTestPoint()).connected(self.conv.pwm_high) + + mcu_touch = self.mcu.with_mixin(IoControllerTouchDriver()) + (self.touch_sink,), _ = self.chain( + mcu_touch.touch.request("touch"), imp.Block(FootprintToucbPad("edg:Symbol_DucklingSolid")) + ) + + self.iron = imp.Block(IronConnector()) + self.connect(self.conv.pwr_out, self.iron.pwr) + + # IRON SENSE AMPS - 3v3 DOMAIN + with self.implicit_connect( ImplicitConnect(self.v3v3, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - rc_filter_model = AnalogLowPassRc(impedance=1*kOhm(tol=0.1), cutoff_freq=(1, 10)*kHertz) - (self.vsense, self.tp_v, self.vfilt), _ = self.chain( - self.conv.pwr_out, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.Block(AnalogTestPoint()), - imp.Block(rc_filter_model), - self.mcu.adc.request('iron_vsense') - ) - (self.ifilt, self.tp_i, self.iamp), _ = self.chain( - self.iron.isense, - imp.Block(Amplifier((18, 25))), - imp.Block(rc_filter_model), - self.Block(AnalogTestPoint()), - self.mcu.adc.request('iron_isense') - ) - - self.tamp = imp.Block(DifferentialAmplifier( - ratio=(150, 165), - input_impedance=(0.9, 5)*kOhm - )) - self.connect(self.tamp.input_negative, self.iron.isense) - self.connect(self.tamp.input_positive, self.iron.thermocouple) - self.connect(self.tamp.output, self.mcu.adc.request('thermocouple')) - self.tp_t = self.Block(AnalogTestPoint()).connected(self.iron.thermocouple) - - @override - def multipack(self) -> None: - self.packed_opamp = self.PackedBlock(Opa2333()) - self.pack(self.packed_opamp.elements.request('0'), ['ifilt', 'amp']) - self.pack(self.packed_opamp.elements.request('1'), ['tamp', 'amp']) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['reg_3v3'], Tps54202h), - (['reg_gate'], L78l), - ], - instance_values=[ - (['refdes_prefix'], 'I'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'vusb_sense=39', - 'i2c.sda=34', - 'i2c.scl=35', - 'pd_int=38', - - 'spk.sd=33', - 'spk.sck=32', - 'spk.ws=31', - - 'pwm_low=4', - 'pwm_high=5', - - 'iron_vsense=6', - 'iron_isense=7', - 'thermocouple=12', - - 'enc_a=10', - 'enc_b=9', - 'enc_sw=8', - 'oled_reset=11', - - 'led=_GPIO0_STRAP', - 'touch=GPIO3', # experimental - ]), - (['mcu', 'programming'], 'uart-auto'), - - (['iron', 'isense_res', 'res', 'res', 'footprint_spec'], 'Resistor_SMD:R_2512_6332Metric'), # more power headroom - (['iron', 'isense_res', 'res', 'res', 'require_basic_part'], False), - - # these will be enforced by the firmware control mechanism - # (['conv', 'pwr_in', 'current_draw'], Range(0, 3)), # max 3A input draw - # force JLC frequency spec - (['conv', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 1e6)), # from charts, inductance constant up to 1MHz - (['reg_3v3', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 11e6)), - - (['reg_gate', 'ic', 'actual_dropout'], Range.exact(0)), # allow tracking - - (['conv', 'sw', 'high_fet', 'part'], ParamValue(['conv', 'sw', 'low_fet', 'part'])), - ], - class_refinements=[ - (HalfBridgeDriver, Ucc27282), - (Speaker, ConnectorSpeaker), - (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified - (EspProgrammingHeader, EspProgrammingTc2030), - (TagConnect, TagConnectNonLegged), - (TestPoint, CompactKeystone5015), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - (Nonstrict3v3Compatible, ['nonstrict_3v3_compatible'], True), - ] - ) + ) as imp: + rc_filter_model = AnalogLowPassRc(impedance=1 * kOhm(tol=0.1), cutoff_freq=(1, 10) * kHertz) + (self.vsense, self.tp_v, self.vfilt), _ = self.chain( + self.conv.pwr_out, + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.Block(AnalogTestPoint()), + imp.Block(rc_filter_model), + self.mcu.adc.request("iron_vsense"), + ) + (self.ifilt, self.tp_i, self.iamp), _ = self.chain( + self.iron.isense, + imp.Block(Amplifier((18, 25))), + imp.Block(rc_filter_model), + self.Block(AnalogTestPoint()), + self.mcu.adc.request("iron_isense"), + ) + + self.tamp = imp.Block(DifferentialAmplifier(ratio=(150, 165), input_impedance=(0.9, 5) * kOhm)) + self.connect(self.tamp.input_negative, self.iron.isense) + self.connect(self.tamp.input_positive, self.iron.thermocouple) + self.connect(self.tamp.output, self.mcu.adc.request("thermocouple")) + self.tp_t = self.Block(AnalogTestPoint()).connected(self.iron.thermocouple) + + @override + def multipack(self) -> None: + self.packed_opamp = self.PackedBlock(Opa2333()) + self.pack(self.packed_opamp.elements.request("0"), ["ifilt", "amp"]) + self.pack(self.packed_opamp.elements.request("1"), ["tamp", "amp"]) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["reg_3v3"], Tps54202h), + (["reg_gate"], L78l), + ], + instance_values=[ + (["refdes_prefix"], "I"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "vusb_sense=39", + "i2c.sda=34", + "i2c.scl=35", + "pd_int=38", + "spk.sd=33", + "spk.sck=32", + "spk.ws=31", + "pwm_low=4", + "pwm_high=5", + "iron_vsense=6", + "iron_isense=7", + "thermocouple=12", + "enc_a=10", + "enc_b=9", + "enc_sw=8", + "oled_reset=11", + "led=_GPIO0_STRAP", + "touch=GPIO3", # experimental + ], + ), + (["mcu", "programming"], "uart-auto"), + ( + ["iron", "isense_res", "res", "res", "footprint_spec"], + "Resistor_SMD:R_2512_6332Metric", + ), # more power headroom + (["iron", "isense_res", "res", "res", "require_basic_part"], False), + # these will be enforced by the firmware control mechanism + # (['conv', 'pwr_in', 'current_draw'], Range(0, 3)), # max 3A input draw + # force JLC frequency spec + ( + ["conv", "power_path", "inductor", "manual_frequency_rating"], + Range(0, 1e6), + ), # from charts, inductance constant up to 1MHz + (["reg_3v3", "power_path", "inductor", "manual_frequency_rating"], Range(0, 11e6)), + (["reg_gate", "ic", "actual_dropout"], Range.exact(0)), # allow tracking + (["conv", "sw", "high_fet", "part"], ParamValue(["conv", "sw", "low_fet", "part"])), + ], + class_refinements=[ + (HalfBridgeDriver, Ucc27282), + (Speaker, ConnectorSpeaker), + (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified + (EspProgrammingHeader, EspProgrammingTc2030), + (TagConnect, TagConnectNonLegged), + (TestPoint, CompactKeystone5015), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + (Nonstrict3v3Compatible, ["nonstrict_3v3_compatible"], True), + ], + ) class IotIronTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(IotIron) + def test_design(self) -> None: + compile_board_inplace(IotIron) diff --git a/examples/test_iot_knob.py b/examples/test_iot_knob.py index d8d3e04ba..d0a980471 100644 --- a/examples/test_iot_knob.py +++ b/examples/test_iot_knob.py @@ -6,162 +6,162 @@ class IotKnob(JlcBoardTop): - """IoT knob with lights, buttons, and a screen. - """ - @override - def contents(self) -> None: - super().contents() - - KNOB_LEDS = 4 # number of RGBs for knob underglow - RING_LEDS = 24 # number of RGBs for the ring indicator - NUM_SECTIONS = 6 # number of buttons - - self.usb = self.Block(UsbCReceptacle(current_limits=(0, 3)*Amp)) - - self.vusb = self.connect(self.usb.pwr) - self.gnd = self.connect(self.usb.gnd) - - self.tp_pwr = self.Block(VoltageTestPoint()).connected(self.usb.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.vusb, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerWifi()) - - self.i2c = self.mcu.i2c.request('i2c') - (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( - self.i2c, - imp.Block(I2cPullup()), imp.Block(I2cTestPoint())) - - # need to name the USB chain so the USB net has the _N and _P postfix for differential traces - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) - - # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request('ledr')) - (self.ledy, ), _ = self.chain(imp.Block(IndicatorLed(Led.Yellow)), self.mcu.gpio.request('ledy')) - - self.enc = imp.Block(DigitalRotaryEncoder()) - self.connect(self.enc.a, self.mcu.gpio.request('enc_a')) - self.connect(self.enc.b, self.mcu.gpio.request('enc_b')) - self.connect(self.enc.with_mixin(DigitalRotaryEncoderSwitch()).sw, self.mcu.gpio.request('enc_sw')) - - self.sw = ElementDict[DigitalSwitch]() - for i in range(NUM_SECTIONS): - (self.sw[i], ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request(f'sw{i}')) - - self.als = imp.Block(Bh1750()) - self.connect(self.i2c, self.als.i2c) - - self.dist = imp.Block(Vl53l0x()) - self.connect(self.i2c, self.dist.i2c) - - self.env = imp.Block(Shtc3()) - self.connect(self.i2c, self.env.i2c) - - self.oled = imp.Block(Er_Oled_096_1_1()) - self.connect(self.i2c, self.oled.i2c) - # TODO ADD POR GENERATOR - self.connect(self.mcu.gpio.request('oled_reset'), self.oled.reset) - - # 5V DOMAIN - with self.implicit_connect( + """IoT knob with lights, buttons, and a screen.""" + + @override + def contents(self) -> None: + super().contents() + + KNOB_LEDS = 4 # number of RGBs for knob underglow + RING_LEDS = 24 # number of RGBs for the ring indicator + NUM_SECTIONS = 6 # number of buttons + + self.usb = self.Block(UsbCReceptacle(current_limits=(0, 3) * Amp)) + + self.vusb = self.connect(self.usb.pwr) + self.gnd = self.connect(self.usb.gnd) + + self.tp_pwr = self.Block(VoltageTestPoint()).connected(self.usb.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.vusb, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerWifi()) + + self.i2c = self.mcu.i2c.request("i2c") + (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( + self.i2c, imp.Block(I2cPullup()), imp.Block(I2cTestPoint()) + ) + + # need to name the USB chain so the USB net has the _N and _P postfix for differential traces + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + # debugging LEDs + (self.ledr,), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request("ledr")) + (self.ledy,), _ = self.chain(imp.Block(IndicatorLed(Led.Yellow)), self.mcu.gpio.request("ledy")) + + self.enc = imp.Block(DigitalRotaryEncoder()) + self.connect(self.enc.a, self.mcu.gpio.request("enc_a")) + self.connect(self.enc.b, self.mcu.gpio.request("enc_b")) + self.connect(self.enc.with_mixin(DigitalRotaryEncoderSwitch()).sw, self.mcu.gpio.request("enc_sw")) + + self.sw = ElementDict[DigitalSwitch]() + for i in range(NUM_SECTIONS): + (self.sw[i],), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request(f"sw{i}")) + + self.als = imp.Block(Bh1750()) + self.connect(self.i2c, self.als.i2c) + + self.dist = imp.Block(Vl53l0x()) + self.connect(self.i2c, self.dist.i2c) + + self.env = imp.Block(Shtc3()) + self.connect(self.i2c, self.env.i2c) + + self.oled = imp.Block(Er_Oled_096_1_1()) + self.connect(self.i2c, self.oled.i2c) + # TODO ADD POR GENERATOR + self.connect(self.mcu.gpio.request("oled_reset"), self.oled.reset) + + # 5V DOMAIN + with self.implicit_connect( ImplicitConnect(self.vusb, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.rgb_shift, self.rgb_tp, self.rgb_knob, self.rgb_ring, self.rgb_sw), _ = self.chain( - self.mcu.gpio.request('rgb'), - imp.Block(L74Ahct1g125()), - imp.Block(DigitalTestPoint()), - imp.Block(NeopixelArrayCircular(KNOB_LEDS)), - imp.Block(NeopixelArrayCircular(RING_LEDS)), - imp.Block(NeopixelArrayCircular(NUM_SECTIONS))) - - self.io8_pur = self.Block(PullupResistor(4.7*kOhm(tol=0.05))) # for ESP32C6 IO8 strapping compatibility - self.connect(self.io8_pur.pwr, self.v3v3) - self.connect(self.io8_pur.io, self.rgb_shift.input) - - (self.spk_dac, self.spk_tp, self.spk_drv, self.spk), _ = self.chain( - self.mcu.gpio.request('spk'), - imp.Block(LowPassRcDac(1*kOhm(tol=0.05), 5*kHertz(tol=0.5))), - self.Block(AnalogTestPoint()), - imp.Block(Pam8302a()), - self.Block(Speaker())) - - (self.v5v_sense, ), _ = self.chain( - self.vusb, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('v5v_sense') - ) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['reg_3v3'], Ldl1117), - ], - instance_values=[ - (['refdes_prefix'], 'K'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - # also designed to be compatible w/ ESP32C6 - # https://www.espressif.com/sites/default/files/documentation/esp32-c6-wroom-1_wroom-1u_datasheet_en.pdf - # note: pin 34 NC, GPIO8 (pin 10) is strapping and should be PUR - # bottom row doesn't exist - 'ledr=25', - 'ledy=24', - - 'sw0=4', - 'sw1=6', - 'sw2=7', - 'sw3=35', - 'sw4=38', - 'sw5=39', - - 'v5v_sense=5', - - 'rgb=10', - 'enc_a=12', - 'enc_b=11', - 'enc_sw=31', - - 'i2c.scl=33', - 'i2c.sda=32', - 'oled_reset=8', - 'spk=9', - ]), - (['mcu', 'programming'], 'uart-auto'), - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (Neopixel, Sk6805_Ec15), - (Speaker, ConnectorSpeaker), - (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified - (TestPoint, CompactKeystone5015), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - (Nonstrict3v3Compatible, ['nonstrict_3v3_compatible'], True), - ] - ) + ) as imp: + (self.rgb_shift, self.rgb_tp, self.rgb_knob, self.rgb_ring, self.rgb_sw), _ = self.chain( + self.mcu.gpio.request("rgb"), + imp.Block(L74Ahct1g125()), + imp.Block(DigitalTestPoint()), + imp.Block(NeopixelArrayCircular(KNOB_LEDS)), + imp.Block(NeopixelArrayCircular(RING_LEDS)), + imp.Block(NeopixelArrayCircular(NUM_SECTIONS)), + ) + + self.io8_pur = self.Block(PullupResistor(4.7 * kOhm(tol=0.05))) # for ESP32C6 IO8 strapping compatibility + self.connect(self.io8_pur.pwr, self.v3v3) + self.connect(self.io8_pur.io, self.rgb_shift.input) + + (self.spk_dac, self.spk_tp, self.spk_drv, self.spk), _ = self.chain( + self.mcu.gpio.request("spk"), + imp.Block(LowPassRcDac(1 * kOhm(tol=0.05), 5 * kHertz(tol=0.5))), + self.Block(AnalogTestPoint()), + imp.Block(Pam8302a()), + self.Block(Speaker()), + ) + + (self.v5v_sense,), _ = self.chain( + self.vusb, + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("v5v_sense"), + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["reg_3v3"], Ldl1117), + ], + instance_values=[ + (["refdes_prefix"], "K"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + # also designed to be compatible w/ ESP32C6 + # https://www.espressif.com/sites/default/files/documentation/esp32-c6-wroom-1_wroom-1u_datasheet_en.pdf + # note: pin 34 NC, GPIO8 (pin 10) is strapping and should be PUR + # bottom row doesn't exist + "ledr=25", + "ledy=24", + "sw0=4", + "sw1=6", + "sw2=7", + "sw3=35", + "sw4=38", + "sw5=39", + "v5v_sense=5", + "rgb=10", + "enc_a=12", + "enc_b=11", + "enc_sw=31", + "i2c.scl=33", + "i2c.sda=32", + "oled_reset=8", + "spk=9", + ], + ), + (["mcu", "programming"], "uart-auto"), + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (Neopixel, Sk6805_Ec15), + (Speaker, ConnectorSpeaker), + (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified + (TestPoint, CompactKeystone5015), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + (Nonstrict3v3Compatible, ["nonstrict_3v3_compatible"], True), + ], + ) class IotKnobTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(IotKnob) + def test_design(self) -> None: + compile_board_inplace(IotKnob) diff --git a/examples/test_iot_led_driver.py b/examples/test_iot_led_driver.py index 07e23f30e..bb88ee8c7 100644 --- a/examples/test_iot_led_driver.py +++ b/examples/test_iot_led_driver.py @@ -6,171 +6,218 @@ class PowerInConnector(Connector): - def __init__(self) -> None: - super().__init__() - self.conn = self.Block(JstShSmHorizontal()) - self.gnd = self.Export(self.conn.pins.request('1').adapt_to(Ground())) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSource( - voltage_out=(10, 16)*Volt, - current_limits=(0, 3)*Amp, - ))) + def __init__(self) -> None: + super().__init__() + self.conn = self.Block(JstShSmHorizontal()) + self.gnd = self.Export(self.conn.pins.request("1").adapt_to(Ground())) + self.pwr = self.Export( + self.conn.pins.request("2").adapt_to( + VoltageSource( + voltage_out=(10, 16) * Volt, + current_limits=(0, 3) * Amp, + ) + ) + ) # note, sent to fabrication with JlcPartsRefinements # JlcBoardTop used here for repeatable builds class IotLedDriver(JlcBoardTop): - """Multichannel IoT high-power external LED driver with a 12v barrel jack input. - """ - @override - def contents(self) -> None: - super().contents() - - # no connectors to save space, just solder to one of the SMD pads - self.pwr = self.Block(PowerInConnector()) - self.gnd = self.connect(self.pwr.gnd) - self.v12 = self.connect(self.pwr.pwr) - self.tp_v12 = self.Block(VoltageTestPoint()).connected(self.pwr.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.pwr.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.v12, - imp.Block(VoltageRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerWifi()) - - # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request('ledr')) - - (self.v12_sense, ), _ = self.chain( - self.v12, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('v12_sense') - ) - - # generic expansion - for qwiic or an encoder w/ button - self.qwiic = self.Block(QwiicTarget()) - self.connect(self.qwiic.gnd, self.gnd) - # DNP the resistor to use the pin as the encoder pushbutton pin - (self.qwiic_pwr_res, ), _ = self.chain( - imp.Block(SeriesPowerResistor(0*Ohm(tol=0))), - self.qwiic.pwr) - self.connect(self.qwiic.pwr.as_digital_source(), self.mcu.gpio.request('qwiic_pwr')) # as sense line - (self.qwiic_i2c, self.qwiic_pull), _ = self.chain( - self.Block(I2cControllerBitBang()).connected_from( - self.mcu.gpio.request('qwiic_scl'), self.mcu.gpio.request('qwiic_sda'), - ), - imp.Block(I2cPullup()), - self.qwiic.i2c) - - self.tof = imp.Block(Vl53l0x()) - self.tof_pull = imp.Block(I2cPullup()) - self.connect(self.mcu.i2c.request('tof'), self.tof_pull.i2c, self.tof.i2c) - - # 12V DOMAIN - self.led_drv = ElementDict[LedDriver]() - self.led_sink = ElementDict[DummyPassive]() - with self.implicit_connect( - ImplicitConnect(self.v12, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - for i in range(4): - led_drv = self.led_drv[i] = imp.Block(LedDriver(max_current=750*mAmp(tol=0.1))) - self.connect(self.mcu.gpio.request(f'led_pwm_{i}'), led_drv.with_mixin(LedDriverPwm()).pwm) + """Multichannel IoT high-power external LED driver with a 12v barrel jack input.""" + + @override + def contents(self) -> None: + super().contents() # no connectors to save space, just solder to one of the SMD pads - leda_sink = self.led_sink[i*2] = imp.Block(DummyPassive()) - self.connect(led_drv.leda, leda_sink.io) - ledk_sink = self.led_sink[i*2+1] = imp.Block(DummyPassive()) - self.connect(led_drv.ledk, ledk_sink.io) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32c3), - (['reg_3v3'], Tps54202h), - (['mcu', 'pi', 'l'], JlcInductor), # breaks on JlcParts - (['mcu', 'pi', 'c2'], JlcCapacitor), # breaks on JlcParts - ], - instance_values=[ - (['refdes_prefix'], 'L'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'ledr=_GPIO9_STRAP', # force using the strapping / boot mode pin - 'qwiic_scl=25', - 'qwiic_sda=26', - 'qwiic_pwr=16', - 'tof.sda=13', - 'tof.scl=12', - 'led_pwm_0=5', - 'led_pwm_1=8', - 'led_pwm_2=9', - 'led_pwm_3=10', - 'v12_sense=4', - ]), - (['mcu', 'programming'], 'uart-auto'), - (['reg_3v3', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 9e6)), - - (['led_drv[0]', 'diode_voltage_drop'], Range(0, 0.5)), - (['led_drv[1]', 'diode_voltage_drop'], ParamValue(['led_drv[0]', 'diode_voltage_drop'])), - (['led_drv[2]', 'diode_voltage_drop'], ParamValue(['led_drv[0]', 'diode_voltage_drop'])), - (['led_drv[3]', 'diode_voltage_drop'], ParamValue(['led_drv[0]', 'diode_voltage_drop'])), - - (['led_drv[0]', 'rsense', 'res', 'res', 'require_basic_part'], False), - (['led_drv[1]', 'rsense', 'res', 'res', 'require_basic_part'], ParamValue(['led_drv[0]', 'rsense', 'res', 'res', 'require_basic_part'])), - (['led_drv[2]', 'rsense', 'res', 'res', 'require_basic_part'], ParamValue(['led_drv[0]', 'rsense', 'res', 'res', 'require_basic_part'])), - (['led_drv[3]', 'rsense', 'res', 'res', 'require_basic_part'], ParamValue(['led_drv[0]', 'rsense', 'res', 'res', 'require_basic_part'])), - (['led_drv[0]', 'power_path', 'inductor', 'inductance'], Range(4.7e-6 * .8, 1.72e-5)), - (['led_drv[0]', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 6.4e6)), - (['led_drv[1]', 'power_path', 'inductor', 'inductance'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'inductance'])), - (['led_drv[1]', 'power_path', 'inductor', 'part'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'part'])), - (['led_drv[1]', 'power_path', 'inductor', 'manual_frequency_rating'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'manual_frequency_rating'])), - (['led_drv[2]', 'power_path', 'inductor', 'inductance'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'inductance'])), - (['led_drv[2]', 'power_path', 'inductor', 'part'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'part'])), - (['led_drv[2]', 'power_path', 'inductor', 'manual_frequency_rating'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'manual_frequency_rating'])), - (['led_drv[3]', 'power_path', 'inductor', 'inductance'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'inductance'])), - (['led_drv[3]', 'power_path', 'inductor', 'part'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'part'])), - (['led_drv[3]', 'power_path', 'inductor', 'manual_frequency_rating'], ParamValue(['led_drv[0]', 'power_path', 'inductor', 'manual_frequency_rating'])), - (['reg_3v3', 'power_path', 'in_cap', 'cap', 'voltage_rating_derating'], 0.80), # use a 1206 25 oe 35v part - (['qwiic', 'pwr', 'current_draw'], Range(0.0, 0.08)), # use 1210 inductor - (['mcu', 'pi', 'c1', 'footprint_area'], Range(4.0, float('inf'))), # use 0603 consistently since that's what's available - (['mcu', 'pi', 'c2', 'footprint_area'], Range(4.0, float('inf'))), - (['mcu', 'pi', 'l', 'footprint_area'], Range(4.0, float('inf'))), - (['reg_3v3', 'fb', 'div', 'top_res', 'footprint_area'], Range(4.0, float('inf'))), - (['reg_3v3', 'fb', 'div', 'bottom_res', 'footprint_area'], Range(4.0, float('inf'))), - (['v12_sense', 'div', 'top_res', 'footprint_area'], Range(4.0, float('inf'))), - (['v12_sense', 'div', 'bottom_res', 'footprint_area'], Range(4.0, float('inf'))), - (['reg_3v3', 'en_res', 'resistance'], Range(100e3, 1e6)), # wider selection of resistors - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (PowerBarrelJack, Pj_036ah), - (Neopixel, Sk6805_Ec15), - (LedDriver, Tps92200), - (RfConnector, UflConnector), - (TestPoint, CompactKeystone5015), - (TagConnect, TagConnectNonLegged), - ], - class_values=[ - (SelectorArea, ['footprint_area'], Range.from_lower(1.5)), # at least 0402 - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - ] - ) + self.pwr = self.Block(PowerInConnector()) + self.gnd = self.connect(self.pwr.gnd) + self.v12 = self.connect(self.pwr.pwr) + self.tp_v12 = self.Block(VoltageTestPoint()).connected(self.pwr.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.pwr.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.v12, + imp.Block(VoltageRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerWifi()) + + # debugging LEDs + (self.ledr,), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request("ledr")) + + (self.v12_sense,), _ = self.chain( + self.v12, + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("v12_sense"), + ) + + # generic expansion - for qwiic or an encoder w/ button + self.qwiic = self.Block(QwiicTarget()) + self.connect(self.qwiic.gnd, self.gnd) + # DNP the resistor to use the pin as the encoder pushbutton pin + (self.qwiic_pwr_res,), _ = self.chain(imp.Block(SeriesPowerResistor(0 * Ohm(tol=0))), self.qwiic.pwr) + self.connect(self.qwiic.pwr.as_digital_source(), self.mcu.gpio.request("qwiic_pwr")) # as sense line + (self.qwiic_i2c, self.qwiic_pull), _ = self.chain( + self.Block(I2cControllerBitBang()).connected_from( + self.mcu.gpio.request("qwiic_scl"), + self.mcu.gpio.request("qwiic_sda"), + ), + imp.Block(I2cPullup()), + self.qwiic.i2c, + ) + + self.tof = imp.Block(Vl53l0x()) + self.tof_pull = imp.Block(I2cPullup()) + self.connect(self.mcu.i2c.request("tof"), self.tof_pull.i2c, self.tof.i2c) + + # 12V DOMAIN + self.led_drv = ElementDict[LedDriver]() + self.led_sink = ElementDict[DummyPassive]() + with self.implicit_connect( + ImplicitConnect(self.v12, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + for i in range(4): + led_drv = self.led_drv[i] = imp.Block(LedDriver(max_current=750 * mAmp(tol=0.1))) + self.connect(self.mcu.gpio.request(f"led_pwm_{i}"), led_drv.with_mixin(LedDriverPwm()).pwm) + + # no connectors to save space, just solder to one of the SMD pads + leda_sink = self.led_sink[i * 2] = imp.Block(DummyPassive()) + self.connect(led_drv.leda, leda_sink.io) + ledk_sink = self.led_sink[i * 2 + 1] = imp.Block(DummyPassive()) + self.connect(led_drv.ledk, ledk_sink.io) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32c3), + (["reg_3v3"], Tps54202h), + (["mcu", "pi", "l"], JlcInductor), # breaks on JlcParts + (["mcu", "pi", "c2"], JlcCapacitor), # breaks on JlcParts + ], + instance_values=[ + (["refdes_prefix"], "L"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "ledr=_GPIO9_STRAP", # force using the strapping / boot mode pin + "qwiic_scl=25", + "qwiic_sda=26", + "qwiic_pwr=16", + "tof.sda=13", + "tof.scl=12", + "led_pwm_0=5", + "led_pwm_1=8", + "led_pwm_2=9", + "led_pwm_3=10", + "v12_sense=4", + ], + ), + (["mcu", "programming"], "uart-auto"), + (["reg_3v3", "power_path", "inductor", "manual_frequency_rating"], Range(0, 9e6)), + (["led_drv[0]", "diode_voltage_drop"], Range(0, 0.5)), + (["led_drv[1]", "diode_voltage_drop"], ParamValue(["led_drv[0]", "diode_voltage_drop"])), + (["led_drv[2]", "diode_voltage_drop"], ParamValue(["led_drv[0]", "diode_voltage_drop"])), + (["led_drv[3]", "diode_voltage_drop"], ParamValue(["led_drv[0]", "diode_voltage_drop"])), + (["led_drv[0]", "rsense", "res", "res", "require_basic_part"], False), + ( + ["led_drv[1]", "rsense", "res", "res", "require_basic_part"], + ParamValue(["led_drv[0]", "rsense", "res", "res", "require_basic_part"]), + ), + ( + ["led_drv[2]", "rsense", "res", "res", "require_basic_part"], + ParamValue(["led_drv[0]", "rsense", "res", "res", "require_basic_part"]), + ), + ( + ["led_drv[3]", "rsense", "res", "res", "require_basic_part"], + ParamValue(["led_drv[0]", "rsense", "res", "res", "require_basic_part"]), + ), + (["led_drv[0]", "power_path", "inductor", "inductance"], Range(4.7e-6 * 0.8, 1.72e-5)), + (["led_drv[0]", "power_path", "inductor", "manual_frequency_rating"], Range(0, 6.4e6)), + ( + ["led_drv[1]", "power_path", "inductor", "inductance"], + ParamValue(["led_drv[0]", "power_path", "inductor", "inductance"]), + ), + ( + ["led_drv[1]", "power_path", "inductor", "part"], + ParamValue(["led_drv[0]", "power_path", "inductor", "part"]), + ), + ( + ["led_drv[1]", "power_path", "inductor", "manual_frequency_rating"], + ParamValue(["led_drv[0]", "power_path", "inductor", "manual_frequency_rating"]), + ), + ( + ["led_drv[2]", "power_path", "inductor", "inductance"], + ParamValue(["led_drv[0]", "power_path", "inductor", "inductance"]), + ), + ( + ["led_drv[2]", "power_path", "inductor", "part"], + ParamValue(["led_drv[0]", "power_path", "inductor", "part"]), + ), + ( + ["led_drv[2]", "power_path", "inductor", "manual_frequency_rating"], + ParamValue(["led_drv[0]", "power_path", "inductor", "manual_frequency_rating"]), + ), + ( + ["led_drv[3]", "power_path", "inductor", "inductance"], + ParamValue(["led_drv[0]", "power_path", "inductor", "inductance"]), + ), + ( + ["led_drv[3]", "power_path", "inductor", "part"], + ParamValue(["led_drv[0]", "power_path", "inductor", "part"]), + ), + ( + ["led_drv[3]", "power_path", "inductor", "manual_frequency_rating"], + ParamValue(["led_drv[0]", "power_path", "inductor", "manual_frequency_rating"]), + ), + ( + ["reg_3v3", "power_path", "in_cap", "cap", "voltage_rating_derating"], + 0.80, + ), # use a 1206 25 oe 35v part + (["qwiic", "pwr", "current_draw"], Range(0.0, 0.08)), # use 1210 inductor + ( + ["mcu", "pi", "c1", "footprint_area"], + Range(4.0, float("inf")), + ), # use 0603 consistently since that's what's available + (["mcu", "pi", "c2", "footprint_area"], Range(4.0, float("inf"))), + (["mcu", "pi", "l", "footprint_area"], Range(4.0, float("inf"))), + (["reg_3v3", "fb", "div", "top_res", "footprint_area"], Range(4.0, float("inf"))), + (["reg_3v3", "fb", "div", "bottom_res", "footprint_area"], Range(4.0, float("inf"))), + (["v12_sense", "div", "top_res", "footprint_area"], Range(4.0, float("inf"))), + (["v12_sense", "div", "bottom_res", "footprint_area"], Range(4.0, float("inf"))), + (["reg_3v3", "en_res", "resistance"], Range(100e3, 1e6)), # wider selection of resistors + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (PowerBarrelJack, Pj_036ah), + (Neopixel, Sk6805_Ec15), + (LedDriver, Tps92200), + (RfConnector, UflConnector), + (TestPoint, CompactKeystone5015), + (TagConnect, TagConnectNonLegged), + ], + class_values=[ + (SelectorArea, ["footprint_area"], Range.from_lower(1.5)), # at least 0402 + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + ], + ) class IotLedDriverTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(IotLedDriver) + def test_design(self) -> None: + compile_board_inplace(IotLedDriver) diff --git a/examples/test_iot_thermal_camera.py b/examples/test_iot_thermal_camera.py index bd7303a13..5b90cf372 100644 --- a/examples/test_iot_thermal_camera.py +++ b/examples/test_iot_thermal_camera.py @@ -6,158 +6,143 @@ class IotThermalCamera(JlcBoardTop): - """Dual-mode IR and RGB camera board with ESP32 - """ - @override - def contents(self) -> None: - super().contents() + """Dual-mode IR and RGB camera board with ESP32""" - self.usb = self.Block(UsbCReceptacle()) - self.gnd = self.connect(self.usb.gnd) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + @override + def contents(self) -> None: + super().contents() - with self.implicit_connect( # POWER + self.usb = self.Block(UsbCReceptacle()) + self.gnd = self.connect(self.usb.gnd) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + + with self.implicit_connect( # POWER + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.choke, self.tp_pwr), _ = self.chain( + self.usb.pwr, self.Block(SeriesPowerFerriteBead()), self.Block(VoltageTestPoint()) + ) + self.pwr = self.connect(self.choke.pwr_out) + + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.pwr, + imp.Block(BuckConverter(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + (self.reg_3v0,), _ = self.chain(self.v3v3, imp.Block(LinearRegulator(output_voltage=3.0 * Volt(tol=0.03)))) + self.v3v0 = self.connect(self.reg_3v0.pwr_out) + + (self.reg_2v8,), _ = self.chain(self.v3v3, imp.Block(LinearRegulator(output_voltage=2.8 * Volt(tol=0.03)))) + self.v2v8 = self.connect(self.reg_2v8.pwr_out) + + (self.reg_1v2,), _ = self.chain(self.v3v3, imp.Block(LinearRegulator(output_voltage=1.2 * Volt(tol=0.03)))) + self.v1v2 = self.connect(self.reg_1v2.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.choke, self.tp_pwr), _ = self.chain( - self.usb.pwr, - self.Block(SeriesPowerFerriteBead()), - self.Block(VoltageTestPoint()) - ) - self.pwr = self.connect(self.choke.pwr_out) - - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.pwr, - imp.Block(BuckConverter(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - (self.reg_3v0, ), _ = self.chain( - self.v3v3, - imp.Block(LinearRegulator(output_voltage=3.0*Volt(tol=0.03))) - ) - self.v3v0 = self.connect(self.reg_3v0.pwr_out) - - (self.reg_2v8, ), _ = self.chain( - self.v3v3, - imp.Block(LinearRegulator(output_voltage=2.8*Volt(tol=0.03))) - ) - self.v2v8 = self.connect(self.reg_2v8.pwr_out) - - (self.reg_1v2, ), _ = self.chain( - self.v3v3, - imp.Block(LinearRegulator(output_voltage=1.2*Volt(tol=0.03))) - ) - self.v1v2 = self.connect(self.reg_1v2.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerWifi()) - - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) - - self.i2c = self.mcu.i2c.request('i2c') - (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( - self.i2c, - imp.Block(I2cPullup()), imp.Block(I2cTestPoint('i2c'))) - - mcu_touch = self.mcu.with_mixin(IoControllerTouchDriver()) - (self.touch_duck, ), _ = self.chain( - mcu_touch.touch.request('touch_duck'), - imp.Block(FootprintToucbPad('edg:Symbol_DucklingSolid')) - ) - - # debugging LEDs - (self.ledr, ), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request('ledr')) - - # CAMERA MULTI DOMAIN - with self.implicit_connect( + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerWifi()) + + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + self.i2c = self.mcu.i2c.request("i2c") + (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( + self.i2c, imp.Block(I2cPullup()), imp.Block(I2cTestPoint("i2c")) + ) + + mcu_touch = self.mcu.with_mixin(IoControllerTouchDriver()) + (self.touch_duck,), _ = self.chain( + mcu_touch.touch.request("touch_duck"), imp.Block(FootprintToucbPad("edg:Symbol_DucklingSolid")) + ) + + # debugging LEDs + (self.ledr,), _ = self.chain(imp.Block(IndicatorLed(Led.Red)), self.mcu.gpio.request("ledr")) + + # CAMERA MULTI DOMAIN + with self.implicit_connect( ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.cam = imp.Block(Ov2640_Fpc24()) - self.connect(self.cam.pwr, self.v3v0) - self.connect(self.cam.pwr_analog, self.v2v8) - self.connect(self.cam.pwr_digital, self.v1v2) - self.connect(self.cam.dvp8, self.mcu.with_mixin(IoControllerDvp8()).dvp8.request('cam')) - self.connect(self.cam.sio, self.i2c) - self.connect(self.cam.reset, self.mcu.gpio.request('cam_rst')) - - self.flir = imp.Block(FlirLepton()) - self.connect(self.flir.pwr_io, self.v3v0) - self.connect(self.flir.pwr, self.v2v8) - self.connect(self.flir.pwr_core, self.v1v2) - self.connect(self.flir.spi, self.mcu.spi.request('flir')) - self.connect(self.flir.cci, self.i2c) - self.connect(self.flir.reset, self.mcu.gpio.request('flir_rst')) - self.connect(self.flir.shutdown, self.mcu.gpio.request('flir_pwrdn')) - self.connect(self.flir.cs, self.mcu.gpio.request('flir_cs')) - self.connect(self.flir.vsync, self.mcu.gpio.request('flir_vsync')) - - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['reg_3v3'], Tps54202h), - (['cam', 'device', 'conn'], Fpc050BottomFlip), - ], - instance_values=[ - (['refdes_prefix'], 'T'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'cam.vsync=25', - 'cam.href=24', - 'cam_rst=23', - 'cam.y7=22', - 'cam.xclk=21', - 'cam.y6=20', - 'cam.y5=15', - 'cam.pclk=19', - 'cam.y4=12', - 'cam.y0=18', - 'cam.y3=10', - 'cam.y1=17', - 'cam.y2=11', - - 'i2c.sda=31', - 'i2c.scl=32', - - 'flir_pwrdn=33', - 'flir_rst=34', - 'flir_cs=38', - 'flir.sck=39', - 'flir.mosi=5', - 'flir.miso=4', - 'flir_vsync=7', - - 'ledr=_GPIO0_STRAP', - - 'touch_duck=6', - ]), - (['mcu', 'programming'], 'uart-auto'), - (['reg_2v8', 'ic', 'actual_dropout'], Range(0.0, 0.05)), # 3.3V @ 100mA - (['reg_3v0', 'ic', 'actual_dropout'], Range(0.0, 0.16)), # 3.3V @ 400mA - (['reg_3v3', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 21e6)), - (['usb', 'pwr', 'current_limits'], Range(0.0, 0.8)), # a bit over - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (TestPoint, CompactKeystone5015), - (LinearRegulator, Tlv757p), # default type for all LDOs - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - ] - ) + ) as imp: + self.cam = imp.Block(Ov2640_Fpc24()) + self.connect(self.cam.pwr, self.v3v0) + self.connect(self.cam.pwr_analog, self.v2v8) + self.connect(self.cam.pwr_digital, self.v1v2) + self.connect(self.cam.dvp8, self.mcu.with_mixin(IoControllerDvp8()).dvp8.request("cam")) + self.connect(self.cam.sio, self.i2c) + self.connect(self.cam.reset, self.mcu.gpio.request("cam_rst")) + + self.flir = imp.Block(FlirLepton()) + self.connect(self.flir.pwr_io, self.v3v0) + self.connect(self.flir.pwr, self.v2v8) + self.connect(self.flir.pwr_core, self.v1v2) + self.connect(self.flir.spi, self.mcu.spi.request("flir")) + self.connect(self.flir.cci, self.i2c) + self.connect(self.flir.reset, self.mcu.gpio.request("flir_rst")) + self.connect(self.flir.shutdown, self.mcu.gpio.request("flir_pwrdn")) + self.connect(self.flir.cs, self.mcu.gpio.request("flir_cs")) + self.connect(self.flir.vsync, self.mcu.gpio.request("flir_vsync")) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["reg_3v3"], Tps54202h), + (["cam", "device", "conn"], Fpc050BottomFlip), + ], + instance_values=[ + (["refdes_prefix"], "T"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "cam.vsync=25", + "cam.href=24", + "cam_rst=23", + "cam.y7=22", + "cam.xclk=21", + "cam.y6=20", + "cam.y5=15", + "cam.pclk=19", + "cam.y4=12", + "cam.y0=18", + "cam.y3=10", + "cam.y1=17", + "cam.y2=11", + "i2c.sda=31", + "i2c.scl=32", + "flir_pwrdn=33", + "flir_rst=34", + "flir_cs=38", + "flir.sck=39", + "flir.mosi=5", + "flir.miso=4", + "flir_vsync=7", + "ledr=_GPIO0_STRAP", + "touch_duck=6", + ], + ), + (["mcu", "programming"], "uart-auto"), + (["reg_2v8", "ic", "actual_dropout"], Range(0.0, 0.05)), # 3.3V @ 100mA + (["reg_3v0", "ic", "actual_dropout"], Range(0.0, 0.16)), # 3.3V @ 400mA + (["reg_3v3", "power_path", "inductor", "manual_frequency_rating"], Range(0, 21e6)), + (["usb", "pwr", "current_limits"], Range(0.0, 0.8)), # a bit over + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (TestPoint, CompactKeystone5015), + (LinearRegulator, Tlv757p), # default type for all LDOs + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + ], + ) class IotThermalCameraTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(IotThermalCamera) + def test_design(self) -> None: + compile_board_inplace(IotThermalCamera) diff --git a/examples/test_jd_keyswitch.py b/examples/test_jd_keyswitch.py index 13793b5ea..6760f0de3 100644 --- a/examples/test_jd_keyswitch.py +++ b/examples/test_jd_keyswitch.py @@ -6,77 +6,77 @@ class JacdacKeyswitch(JacdacDeviceTop, JlcBoardTop): - """A Jacdac socketed mechanical keyswitch with RGB, for the gamer-maker in all of us. - """ - @override - def contents(self) -> None: - super().contents() + """A Jacdac socketed mechanical keyswitch with RGB, for the gamer-maker in all of us.""" - self.edge2 = self.create_edge() + @override + def contents(self) -> None: + super().contents() - # TODO should connect to the nets, once .connected can take a Connection - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.edge2.gnd) - self.tp_jd_pwr = self.Block(VoltageTestPoint()).connected(self.edge2.jd_pwr_sink) + self.edge2 = self.create_edge() - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3), _ = self.chain( - self.jd_pwr, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) + # TODO should connect to the nets, once .connected can take a Connection + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.edge2.gnd) + self.tp_jd_pwr = self.Block(VoltageTestPoint()).connected(self.edge2.jd_pwr_sink) - # 3V3 DOMAIN - with self.implicit_connect( + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3), _ = self.chain( + self.jd_pwr, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( ImplicitConnect(self.v3v3, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.sw, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw')) - (self.rgb, ), _ = self.chain(imp.Block(IndicatorSinkRgbLed()), self.mcu.gpio.request_vector('rgb')) - - (self.jd_if, ), _ = self.chain(self.mcu.gpio.request('jd_data'), - imp.Block(JacdacDataInterface()), - self.jd_data) - - self.connect(self.mcu.gpio.request('jd_status'), self.jd_status) - - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Stm32g031_G), - (['reg_3v3'], Xc6209), # up to 10V input, more robust in case of transients - ], - class_refinements=[ - (TvsDiode, Rclamp0521p), - (Switch, KailhSocket), - (SwdCortexTargetHeader, SwdCortexTargetTagConnect), - (TagConnect, TagConnectNonLegged), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - # pinning based on https://github.com/microsoft/jacdac-ddk/blob/main/electronics/altium/module-designs/JacdacDevRgbEc30%20117-1.0/PDF/JacdacDevRgbEc30%20117-1.0%20schematic.PDF - 'jd_data=PB6', # PB3/4/5/6 on reference design - 'jd_status=PC14', - - 'rgb_blue=15', - 'rgb_red=16', - 'rgb_green=17', - - 'sw=19', - ]), - (['edge', 'status_led', 'color'], 'yellow'), # NONSTANDARD, but uses a JLC basic part - (['edge2', 'status_led', 'color'], 'yellow'), - ], - ) + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.sw,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw")) + (self.rgb,), _ = self.chain(imp.Block(IndicatorSinkRgbLed()), self.mcu.gpio.request_vector("rgb")) + + (self.jd_if,), _ = self.chain( + self.mcu.gpio.request("jd_data"), imp.Block(JacdacDataInterface()), self.jd_data + ) + + self.connect(self.mcu.gpio.request("jd_status"), self.jd_status) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Stm32g031_G), + (["reg_3v3"], Xc6209), # up to 10V input, more robust in case of transients + ], + class_refinements=[ + (TvsDiode, Rclamp0521p), + (Switch, KailhSocket), + (SwdCortexTargetHeader, SwdCortexTargetTagConnect), + (TagConnect, TagConnectNonLegged), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + # pinning based on https://github.com/microsoft/jacdac-ddk/blob/main/electronics/altium/module-designs/JacdacDevRgbEc30%20117-1.0/PDF/JacdacDevRgbEc30%20117-1.0%20schematic.PDF + "jd_data=PB6", # PB3/4/5/6 on reference design + "jd_status=PC14", + "rgb_blue=15", + "rgb_red=16", + "rgb_green=17", + "sw=19", + ], + ), + (["edge", "status_led", "color"], "yellow"), # NONSTANDARD, but uses a JLC basic part + (["edge2", "status_led", "color"], "yellow"), + ], + ) class JacdacKeyswitchTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(JacdacKeyswitch) + def test_design(self) -> None: + compile_board_inplace(JacdacKeyswitch) diff --git a/examples/test_keyboard.py b/examples/test_keyboard.py index 1a508a476..028109bc4 100644 --- a/examples/test_keyboard.py +++ b/examples/test_keyboard.py @@ -16,35 +16,35 @@ class Keyboard(SimpleBoardTop): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.usb = self.Block(UsbCReceptacle()) - self.reg = self.Block(Ldl1117(3.3*Volt(tol=0.05))) - self.connect(self.usb.gnd, self.reg.gnd) - self.connect(self.usb.pwr, self.reg.pwr_in) + self.usb = self.Block(UsbCReceptacle()) + self.reg = self.Block(Ldl1117(3.3 * Volt(tol=0.05))) + self.connect(self.usb.gnd, self.reg.gnd) + self.connect(self.usb.pwr, self.reg.pwr_in) - with self.implicit_connect( + with self.implicit_connect( ImplicitConnect(self.reg.pwr_out, [Power]), ImplicitConnect(self.reg.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(Stm32f103_48()) - self.connect(self.usb.usb, self.mcu.usb.request()) + ) as imp: + self.mcu = imp.Block(Stm32f103_48()) + self.connect(self.usb.usb, self.mcu.usb.request()) - self.sw = self.Block(SwitchMatrix(nrows=3, ncols=2)) - self.connect(self.sw.cols, self.mcu.gpio.request_vector()) - self.connect(self.sw.rows, self.mcu.gpio.request_vector()) + self.sw = self.Block(SwitchMatrix(nrows=3, ncols=2)) + self.connect(self.sw.cols, self.mcu.gpio.request_vector()) + self.connect(self.sw.rows, self.mcu.gpio.request_vector()) - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - class_refinements=[ - (Switch, KailhSocket), - ], - ) + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + class_refinements=[ + (Switch, KailhSocket), + ], + ) class KeyboardTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(Keyboard) + def test_design(self) -> None: + compile_board_inplace(Keyboard) diff --git a/examples/test_ledmatrix.py b/examples/test_ledmatrix.py index d67e05be5..1562839fa 100644 --- a/examples/test_ledmatrix.py +++ b/examples/test_ledmatrix.py @@ -6,67 +6,70 @@ class LedMatrix(JlcBoardTop): - """A USB-connected WiFi-enabled LED matrix that demonstrates a charlieplexing LED matrix generator. - """ - @override - def contents(self) -> None: - super().contents() + """A USB-connected WiFi-enabled LED matrix that demonstrates a charlieplexing LED matrix generator.""" - self.usb = self.Block(UsbCReceptacle()) + @override + def contents(self) -> None: + super().contents() - self.vusb = self.connect(self.usb.pwr) - self.gnd = self.connect(self.usb.gnd) + self.usb = self.Block(UsbCReceptacle()) - self.tp_vusb = self.Block(VoltageTestPoint()).connected(self.usb.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + self.vusb = self.connect(self.usb.pwr) + self.gnd = self.connect(self.usb.gnd) - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.vusb, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) + self.tp_vusb = self.Block(VoltageTestPoint()).connected(self.usb.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.vusb, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) - (self.sw1, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw1')) + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) - # maximum current draw that is still within the column sink capability of the ESP32 - self.matrix = imp.Block(CharlieplexedLedMatrix(6, 5, current_draw=(3.5, 5)*mAmp, color=Led.Yellow)) - self.connect(self.mcu.gpio.request_vector('led'), self.matrix.ios) + (self.sw1,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw1")) - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32c3_Wroom02), - (['reg_3v3'], Ldl1117), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'led_0=3', - 'led_1=4', - 'led_2=5', - 'led_3=6', - 'led_4=17', - 'led_5=15', - 'led_6=10', - 'sw1=18', - ]), - ], - ) + # maximum current draw that is still within the column sink capability of the ESP32 + self.matrix = imp.Block(CharlieplexedLedMatrix(6, 5, current_draw=(3.5, 5) * mAmp, color=Led.Yellow)) + self.connect(self.mcu.gpio.request_vector("led"), self.matrix.ios) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32c3_Wroom02), + (["reg_3v3"], Ldl1117), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "led_0=3", + "led_1=4", + "led_2=5", + "led_3=6", + "led_4=17", + "led_5=15", + "led_6=10", + "sw1=18", + ], + ), + ], + ) class LedMatrixTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(LedMatrix) + def test_design(self) -> None: + compile_board_inplace(LedMatrix) diff --git a/examples/test_lora.py b/examples/test_lora.py index c8c494cb5..96aa12521 100644 --- a/examples/test_lora.py +++ b/examples/test_lora.py @@ -6,214 +6,222 @@ class EspLora(JlcBoardTop): - """ESP32 + discrete 915MHz LoRa via SX1262. USB-C powered. - TODO: add RF TVS diode to avoid device damage - TODO: for future versions: use TCXO for SX1262, connect BUSY pin - - Compatible with Meshtastic, with these notes for build configuration: - - SX126X_DIO3_TCXO_VOLTAGE must not be defined (this design uses a crystal) - - SX126X_BUSY should be defined to -1 (BUSY not connected) - - SX126X_DIO2, SX126X_DIO3 can be left undefined (not connected to microcontroller) - - variant.h defines: - #define BUTTON_PIN 0 // This is the BOOT button - #define BUTTON_NEED_PULLUP - - #define USE_SX1262 - - #define LORA_SCK 5 - #define LORA_MISO 3 - #define LORA_MOSI 6 - #define LORA_CS 7 - #define LORA_RESET 8 - #define LORA_DIO1 38 - - #ifdef USE_SX1262 - #define SX126X_CS LORA_CS - #define SX126X_DIO1 LORA_DIO1 - #define SX126X_RESET LORA_RESET - #define SX126X_DIO2_AS_RF_SWITCH - #define SX126X_BUSY -1 - #endif - - pins_arduino.h defines: - static const uint8_t SDA = 18; - static const uint8_t SCL = 17; - - static const uint8_t SS = 7; - static const uint8_t MOSI = 6; - static const uint8_t MISO = 3; - static const uint8_t SCK = 5; - - #define SPI_MOSI (11) - #define SPI_SCK (14) - #define SPI_MISO (2) - #define SPI_CS (13) - - #define SDCARD_CS SPI_CS - """ - @override - def contents(self) -> None: - super().contents() - - self.usb = self.Block(UsbCReceptacle()) - self.gnd = self.connect(self.usb.gnd) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) - - with self.implicit_connect( # POWER - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.choke, self.tp_pwr), _ = self.chain( - self.usb.pwr, - self.Block(SeriesPowerFerriteBead()), - self.Block(VoltageTestPoint()) - ) - self.pwr = self.connect(self.choke.pwr_out) - - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.pwr, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - with self.implicit_connect( # 3V3 DOMAIN - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerBle()) - - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) - - (self.ledr, ), _ = self.chain(self.mcu.gpio.request('ledr'), imp.Block(IndicatorLed(Led.Red))) - (self.ledg, ), _ = self.chain(self.mcu.gpio.request('ledg'), imp.Block(IndicatorLed(Led.Yellow))) - (self.ledb, ), _ = self.chain(self.mcu.gpio.request('ledb'), imp.Block(IndicatorLed(Led.White))) - - self.lora = imp.Block(Sx1262()) - (self.tp_lora_spi, ), _ = self.chain(self.mcu.spi.request('lora'), imp.Block(SpiTestPoint('lr')), self.lora.spi) - (self.tp_lora_cs, ), _ = self.chain(self.mcu.gpio.request('lora_cs'), imp.Block(DigitalTestPoint('lr_cs')), - self.lora.cs) - (self.tp_lora_rst, ), _ = self.chain(self.mcu.gpio.request('lora_rst'), imp.Block(DigitalTestPoint('lr_rs')), - self.lora.reset) - (self.tp_lora_dio, ), _ = self.chain(self.mcu.gpio.request('lora_irq'), imp.Block(DigitalTestPoint('lr_di')), - self.lora.irq) - (self.tp_lora_busy, ), _ = self.chain(self.mcu.gpio.request('lora_busy'), imp.Block(DigitalTestPoint('lr_bs')), - self.lora.busy) - - self.i2c = self.mcu.i2c.request('i2c') - (self.i2c_pull, self.i2c_tp), _ = self.chain(self.i2c, imp.Block(I2cPullup()), self.Block(I2cTestPoint('i2c'))) - - self.oled = imp.Block(Er_Oled_096_1_1()) - self.connect(self.i2c, self.oled.i2c) - (self.oled_rst, self.oled_pull), _ = self.chain( - imp.Block(Apx803s()), # -29 variant used on Adafruit boards - imp.Block(PullupResistor(10*kOhm(tol=0.05))), - self.oled.reset - ) - - self.sd = imp.Block(SdCard()) - self.connect(self.mcu.spi.request('sd'), self.sd.spi) - self.connect(self.mcu.gpio.request('sd_cs'), self.sd.cs) - - self.nfc = imp.Block(Pn7160()) - self.connect(self.nfc.pwr, self.pwr) - self.connect(self.nfc.pwr_io, self.v3v3) - self.connect(self.nfc.i2c, self.i2c) - self.connect(self.nfc.reset, self.mcu.gpio.request('nfc_rst')) - self.connect(self.nfc.irq, self.mcu.gpio.request('nfc_irq')) - - @override - def multipack(self) -> None: - self.tx_cpack = self.PackedBlock(CombinedCapacitor()) - self.pack(self.tx_cpack.elements.request('0'), ['lora', 'tx_l', 'c']) - self.pack(self.tx_cpack.elements.request('1'), ['lora', 'tx_pi', 'c1']) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['reg_3v3'], Ldl1117), - (['lora', 'ant'], RfConnectorAntenna), - (['lora', 'ant', 'conn'], Amphenol901143), - ], - instance_values=[ - (['refdes_prefix'], 'L'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - # LoRa and OLED pinnings consistent with Lilygo T3S3 - 'lora.mosi=GPIO6', - 'lora.sck=GPIO5', - 'lora_cs=GPIO7', - 'lora_rst=GPIO8', - 'lora.miso=GPIO3', - 'lora_irq=GPIO38', # IO33 on original, but is a PSRAM pin - 'lora_busy=GPIO40', # IO34 on original, but is a PSRAM pin - 'i2c.sda=GPIO18', - 'i2c.scl=GPIO17', - 'sd_cs=GPIO13', - 'sd.mosi=GPIO11', - 'sd.sck=GPIO14', - 'sd.miso=GPIO2', - - 'nfc_rst=32', - 'nfc_irq=GPIO47', - - 'ledr=34', - 'ledg=35', - 'ledb=39', - ]), - (['mcu', 'programming'], 'uart-auto-button'), - - (['usb', 'conn', 'current_limits'], Range(0.0, 1.1)), # fudge it a lot - (['pwr', 'current_drawn'], Range(0.031392638, 0.8)), # allow use of basic part ferrite, assume not everything run simultaneously - - (['lora', 'balun', 'c', 'capacitance'], Range(2.8e-12 * 0.8, 2.8e-12 * 1.2)), # extend tolerance to find a part - (['lora', 'dcc_l', 'manual_frequency_rating'], Range(0, 20e6)), - (['nfc', 'emc', 'l1', 'manual_frequency_rating'], Range(0, 100e6)), - (['nfc', 'emc', 'l2', 'manual_frequency_rating'], Range(0, 100e6)), - # these RF passives aren't common / basic parts and will be DNP'd anyways - (['lora', 'tx_l', 'c_lc', 'require_basic_part'], False), - (['lora', 'tx_pi', 'c2', 'require_basic_part'], False), - (['lora', 'balun', 'c_p', 'require_basic_part'], False), - (['lora', 'ant_pi', 'c1', 'require_basic_part'], False), - (['lora', 'ant_pi', 'c2', 'require_basic_part'], False), - - (['nfc', 'emc', 'c1', 'require_basic_part'], False), - (['nfc', 'emc', 'c2', 'require_basic_part'], False), - (['nfc', 'damp', 'r1', 'require_basic_part'], False), - (['nfc', 'damp', 'r2', 'require_basic_part'], False), - (['nfc', 'match', 'cp1', 'require_basic_part'], False), - (['nfc', 'match', 'cp2', 'require_basic_part'], False), - - (['nfc', 'cvdd1', 'cap', 'footprint_spec'], "Capacitor_SMD:C_0805_2012Metric"), - (['nfc', 'cvdd2', 'cap', 'footprint_spec'], "Capacitor_SMD:C_0805_2012Metric"), - (['nfc', 'ctvdd1', 'cap', 'footprint_spec'], "Capacitor_SMD:C_0805_2012Metric"), - (['nfc', 'ctvdd2', 'cap', 'footprint_spec'], "Capacitor_SMD:C_0805_2012Metric"), - (['nfc', 'cvdd1', 'cap', 'require_basic_part'], False), - (['nfc', 'cvdd2', 'cap', 'require_basic_part'], False), - (['nfc', 'ctvdd1', 'cap', 'require_basic_part'], False), - (['nfc', 'ctvdd2', 'cap', 'require_basic_part'], False), - - # got bamboozled, this will be replaced in the future with a part from a non-awful vendor - (['lora', 'rf_sw', 'ic', 'restricted_availiability'], False), - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (SdCard, Molex1040310811), - (TestPoint, CompactKeystone5015), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), - (Nonstrict3v3Compatible, ['nonstrict_3v3_compatible'], True), - (DiscreteRfWarning, ['discrete_rf_warning'], False), - (Er_Oled_096_1_1, ['iref_res', 'resistance'], Range.from_tolerance(470e3, 0.1)), - ] - ) + """ESP32 + discrete 915MHz LoRa via SX1262. USB-C powered. + TODO: add RF TVS diode to avoid device damage + TODO: for future versions: use TCXO for SX1262, connect BUSY pin + + Compatible with Meshtastic, with these notes for build configuration: + - SX126X_DIO3_TCXO_VOLTAGE must not be defined (this design uses a crystal) + - SX126X_BUSY should be defined to -1 (BUSY not connected) + - SX126X_DIO2, SX126X_DIO3 can be left undefined (not connected to microcontroller) + + variant.h defines: + #define BUTTON_PIN 0 // This is the BOOT button + #define BUTTON_NEED_PULLUP + + #define USE_SX1262 + + #define LORA_SCK 5 + #define LORA_MISO 3 + #define LORA_MOSI 6 + #define LORA_CS 7 + #define LORA_RESET 8 + #define LORA_DIO1 38 + + #ifdef USE_SX1262 + #define SX126X_CS LORA_CS + #define SX126X_DIO1 LORA_DIO1 + #define SX126X_RESET LORA_RESET + #define SX126X_DIO2_AS_RF_SWITCH + #define SX126X_BUSY -1 + #endif + + pins_arduino.h defines: + static const uint8_t SDA = 18; + static const uint8_t SCL = 17; + + static const uint8_t SS = 7; + static const uint8_t MOSI = 6; + static const uint8_t MISO = 3; + static const uint8_t SCK = 5; + + #define SPI_MOSI (11) + #define SPI_SCK (14) + #define SPI_MISO (2) + #define SPI_CS (13) + + #define SDCARD_CS SPI_CS + """ + + @override + def contents(self) -> None: + super().contents() + + self.usb = self.Block(UsbCReceptacle()) + self.gnd = self.connect(self.usb.gnd) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + + with self.implicit_connect( # POWER + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.choke, self.tp_pwr), _ = self.chain( + self.usb.pwr, self.Block(SeriesPowerFerriteBead()), self.Block(VoltageTestPoint()) + ) + self.pwr = self.connect(self.choke.pwr_out) + + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.pwr, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + with self.implicit_connect( # 3V3 DOMAIN + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerBle()) + + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + (self.ledr,), _ = self.chain(self.mcu.gpio.request("ledr"), imp.Block(IndicatorLed(Led.Red))) + (self.ledg,), _ = self.chain(self.mcu.gpio.request("ledg"), imp.Block(IndicatorLed(Led.Yellow))) + (self.ledb,), _ = self.chain(self.mcu.gpio.request("ledb"), imp.Block(IndicatorLed(Led.White))) + + self.lora = imp.Block(Sx1262()) + (self.tp_lora_spi,), _ = self.chain( + self.mcu.spi.request("lora"), imp.Block(SpiTestPoint("lr")), self.lora.spi + ) + (self.tp_lora_cs,), _ = self.chain( + self.mcu.gpio.request("lora_cs"), imp.Block(DigitalTestPoint("lr_cs")), self.lora.cs + ) + (self.tp_lora_rst,), _ = self.chain( + self.mcu.gpio.request("lora_rst"), imp.Block(DigitalTestPoint("lr_rs")), self.lora.reset + ) + (self.tp_lora_dio,), _ = self.chain( + self.mcu.gpio.request("lora_irq"), imp.Block(DigitalTestPoint("lr_di")), self.lora.irq + ) + (self.tp_lora_busy,), _ = self.chain( + self.mcu.gpio.request("lora_busy"), imp.Block(DigitalTestPoint("lr_bs")), self.lora.busy + ) + + self.i2c = self.mcu.i2c.request("i2c") + (self.i2c_pull, self.i2c_tp), _ = self.chain( + self.i2c, imp.Block(I2cPullup()), self.Block(I2cTestPoint("i2c")) + ) + + self.oled = imp.Block(Er_Oled_096_1_1()) + self.connect(self.i2c, self.oled.i2c) + (self.oled_rst, self.oled_pull), _ = self.chain( + imp.Block(Apx803s()), # -29 variant used on Adafruit boards + imp.Block(PullupResistor(10 * kOhm(tol=0.05))), + self.oled.reset, + ) + + self.sd = imp.Block(SdCard()) + self.connect(self.mcu.spi.request("sd"), self.sd.spi) + self.connect(self.mcu.gpio.request("sd_cs"), self.sd.cs) + + self.nfc = imp.Block(Pn7160()) + self.connect(self.nfc.pwr, self.pwr) + self.connect(self.nfc.pwr_io, self.v3v3) + self.connect(self.nfc.i2c, self.i2c) + self.connect(self.nfc.reset, self.mcu.gpio.request("nfc_rst")) + self.connect(self.nfc.irq, self.mcu.gpio.request("nfc_irq")) + + @override + def multipack(self) -> None: + self.tx_cpack = self.PackedBlock(CombinedCapacitor()) + self.pack(self.tx_cpack.elements.request("0"), ["lora", "tx_l", "c"]) + self.pack(self.tx_cpack.elements.request("1"), ["lora", "tx_pi", "c1"]) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["reg_3v3"], Ldl1117), + (["lora", "ant"], RfConnectorAntenna), + (["lora", "ant", "conn"], Amphenol901143), + ], + instance_values=[ + (["refdes_prefix"], "L"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + # LoRa and OLED pinnings consistent with Lilygo T3S3 + "lora.mosi=GPIO6", + "lora.sck=GPIO5", + "lora_cs=GPIO7", + "lora_rst=GPIO8", + "lora.miso=GPIO3", + "lora_irq=GPIO38", # IO33 on original, but is a PSRAM pin + "lora_busy=GPIO40", # IO34 on original, but is a PSRAM pin + "i2c.sda=GPIO18", + "i2c.scl=GPIO17", + "sd_cs=GPIO13", + "sd.mosi=GPIO11", + "sd.sck=GPIO14", + "sd.miso=GPIO2", + "nfc_rst=32", + "nfc_irq=GPIO47", + "ledr=34", + "ledg=35", + "ledb=39", + ], + ), + (["mcu", "programming"], "uart-auto-button"), + (["usb", "conn", "current_limits"], Range(0.0, 1.1)), # fudge it a lot + ( + ["pwr", "current_drawn"], + Range(0.031392638, 0.8), + ), # allow use of basic part ferrite, assume not everything run simultaneously + ( + ["lora", "balun", "c", "capacitance"], + Range(2.8e-12 * 0.8, 2.8e-12 * 1.2), + ), # extend tolerance to find a part + (["lora", "dcc_l", "manual_frequency_rating"], Range(0, 20e6)), + (["nfc", "emc", "l1", "manual_frequency_rating"], Range(0, 100e6)), + (["nfc", "emc", "l2", "manual_frequency_rating"], Range(0, 100e6)), + # these RF passives aren't common / basic parts and will be DNP'd anyways + (["lora", "tx_l", "c_lc", "require_basic_part"], False), + (["lora", "tx_pi", "c2", "require_basic_part"], False), + (["lora", "balun", "c_p", "require_basic_part"], False), + (["lora", "ant_pi", "c1", "require_basic_part"], False), + (["lora", "ant_pi", "c2", "require_basic_part"], False), + (["nfc", "emc", "c1", "require_basic_part"], False), + (["nfc", "emc", "c2", "require_basic_part"], False), + (["nfc", "damp", "r1", "require_basic_part"], False), + (["nfc", "damp", "r2", "require_basic_part"], False), + (["nfc", "match", "cp1", "require_basic_part"], False), + (["nfc", "match", "cp2", "require_basic_part"], False), + (["nfc", "cvdd1", "cap", "footprint_spec"], "Capacitor_SMD:C_0805_2012Metric"), + (["nfc", "cvdd2", "cap", "footprint_spec"], "Capacitor_SMD:C_0805_2012Metric"), + (["nfc", "ctvdd1", "cap", "footprint_spec"], "Capacitor_SMD:C_0805_2012Metric"), + (["nfc", "ctvdd2", "cap", "footprint_spec"], "Capacitor_SMD:C_0805_2012Metric"), + (["nfc", "cvdd1", "cap", "require_basic_part"], False), + (["nfc", "cvdd2", "cap", "require_basic_part"], False), + (["nfc", "ctvdd1", "cap", "require_basic_part"], False), + (["nfc", "ctvdd2", "cap", "require_basic_part"], False), + # got bamboozled, this will be replaced in the future with a part from a non-awful vendor + (["lora", "rf_sw", "ic", "restricted_availiability"], False), + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (SdCard, Molex1040310811), + (TestPoint, CompactKeystone5015), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), + (Nonstrict3v3Compatible, ["nonstrict_3v3_compatible"], True), + (DiscreteRfWarning, ["discrete_rf_warning"], False), + (Er_Oled_096_1_1, ["iref_res", "resistance"], Range.from_tolerance(470e3, 0.1)), + ], + ) class EspLoraTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(EspLora) + def test_design(self) -> None: + compile_board_inplace(EspLora) diff --git a/examples/test_multimeter.py b/examples/test_multimeter.py index ff7cc2943..31e0a44da 100644 --- a/examples/test_multimeter.py +++ b/examples/test_multimeter.py @@ -7,405 +7,425 @@ class ResistorMux(Interface, KiCadImportableBlock, GeneratorBlock): - """Generates an array of resistors with one side muxed and the other end an array. Passive-typed. - Specify an infinite resistance for an open circuit.""" - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'edg_importable:ResistorMux' - return { - 'control': self.control, 'sw': self.com, 'com': self.input, - 'V+': self.pwr, 'V-': self.gnd - } - - def __init__(self, resistances: ArrayRangeLike): - super().__init__() - - self.switch = self.Block(AnalogSwitch()) - - self.pwr = self.Export(self.switch.pwr, [Power]) - self.gnd = self.Export(self.switch.gnd, [Common]) - - self.control = self.Export(self.switch.control) - self.input = self.Port(Passive.empty()) # resistor side - self.com = self.Export(self.switch.com) # switch side - - self.resistances = self.ArgParameter(resistances) - self.generator_param(self.resistances) - - @override - def generate(self) -> None: - super().generate() - self.res = ElementDict[Resistor]() - for i, resistance in enumerate(self.get(self.resistances)): - if resistance.upper == float('inf'): # open circuit for this step - self.dummy = self.Block(DummyPassive()) - self.connect(self.dummy.io, self.switch.inputs.request(str(i))) - else: - res = self.res[i] = self.Block(Resistor(resistance)) - self.connect(res.a, self.input) - self.connect(res.b, self.switch.inputs.request(str(i))) + """Generates an array of resistors with one side muxed and the other end an array. Passive-typed. + Specify an infinite resistance for an open circuit.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name == "edg_importable:ResistorMux" + return {"control": self.control, "sw": self.com, "com": self.input, "V+": self.pwr, "V-": self.gnd} + + def __init__(self, resistances: ArrayRangeLike): + super().__init__() + + self.switch = self.Block(AnalogSwitch()) + + self.pwr = self.Export(self.switch.pwr, [Power]) + self.gnd = self.Export(self.switch.gnd, [Common]) + + self.control = self.Export(self.switch.control) + self.input = self.Port(Passive.empty()) # resistor side + self.com = self.Export(self.switch.com) # switch side + + self.resistances = self.ArgParameter(resistances) + self.generator_param(self.resistances) + + @override + def generate(self) -> None: + super().generate() + self.res = ElementDict[Resistor]() + for i, resistance in enumerate(self.get(self.resistances)): + if resistance.upper == float("inf"): # open circuit for this step + self.dummy = self.Block(DummyPassive()) + self.connect(self.dummy.io, self.switch.inputs.request(str(i))) + else: + res = self.res[i] = self.Block(Resistor(resistance)) + self.connect(res.a, self.input) + self.connect(res.b, self.switch.inputs.request(str(i))) class MultimeterAnalog(KiCadSchematicBlock, Block): - """Analog measurement stage for the volts stage of the multimeter. - Includes a 1M input resistor and a variable divider. - Purely DC sampling, and true-RMS functionality needs to be implemented in firmware - - TODO: support wider ranges, to be implemented with port array support - """ - def __init__(self) -> None: - super().__init__() - - # TODO: separate Vref? - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.input_positive = self.Port(AnalogSink.empty()) - self.input_negative = self.Port(AnalogSink.empty()) - self.output = self.Port(AnalogSource.empty()) - - self.select = self.Port(Vector(DigitalSink.empty())) - - @override - def contents(self) -> None: - super().contents() - - self.res = self.Block(Resistor(1*MOhm(tol=0.01), voltage=self.input_positive.link().voltage)) - self.range = self.Block(ResistorMux([ - 1*kOhm(tol=0.01), # 1:1000 step (+/- 1 kV range) - 10*kOhm(tol=0.01), # 1:100 step (+/- 100 V range) - 100*kOhm(tol=0.01), # 1:10 step (+/- 10 V range) - Range(float('inf'), float('inf')) # 1:1 step, open circuit - ])) - - output_voltage = self.pwr.link().voltage.hull(self.gnd.link().voltage) - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'input_positive': AnalogSink(), - 'output': AnalogSource( # assumed clamped by the switch in the resistor mux - voltage_out=output_voltage, - signal_out=output_voltage, - current_limits=(-10, 10)*mAmp, - impedance=1*mOhm(tol=0) - ), - 'input_negative': AnalogSink(), - }) + """Analog measurement stage for the volts stage of the multimeter. + Includes a 1M input resistor and a variable divider. + Purely DC sampling, and true-RMS functionality needs to be implemented in firmware + + TODO: support wider ranges, to be implemented with port array support + """ + + def __init__(self) -> None: + super().__init__() + + # TODO: separate Vref? + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.input_positive = self.Port(AnalogSink.empty()) + self.input_negative = self.Port(AnalogSink.empty()) + self.output = self.Port(AnalogSource.empty()) + + self.select = self.Port(Vector(DigitalSink.empty())) + + @override + def contents(self) -> None: + super().contents() + + self.res = self.Block(Resistor(1 * MOhm(tol=0.01), voltage=self.input_positive.link().voltage)) + self.range = self.Block( + ResistorMux( + [ + 1 * kOhm(tol=0.01), # 1:1000 step (+/- 1 kV range) + 10 * kOhm(tol=0.01), # 1:100 step (+/- 100 V range) + 100 * kOhm(tol=0.01), # 1:10 step (+/- 10 V range) + Range(float("inf"), float("inf")), # 1:1 step, open circuit + ] + ) + ) + + output_voltage = self.pwr.link().voltage.hull(self.gnd.link().voltage) + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "input_positive": AnalogSink(), + "output": AnalogSource( # assumed clamped by the switch in the resistor mux + voltage_out=output_voltage, + signal_out=output_voltage, + current_limits=(-10, 10) * mAmp, + impedance=1 * mOhm(tol=0), + ), + "input_negative": AnalogSink(), + }, + ) class MultimeterCurrentDriver(KiCadSchematicBlock, Block): - """Protected constant-current stage for the multimeter driver. - """ - def __init__(self, voltage_rating: RangeLike = RangeExpr()): - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.output = self.Port(AnalogSink.empty()) # TBD this should be some kind of AnalogBidirectional - - self.control = self.Port(AnalogSink.empty()) - self.select = self.Port(Vector(DigitalSink.empty())) - self.enable = self.Port(DigitalSink.empty()) - - self.voltage_rating = self.ArgParameter(voltage_rating) - - @override - def contents(self) -> None: - super().contents() - max_in_voltage = self.control.link().voltage.upper() - - self.fet = self.Block(Fet.PFet( - drain_voltage=self.voltage_rating, # protect against negative overvoltage - drain_current=(0, max_in_voltage / 1000), # approx lowest resistance - TODO properly model the resistor mux - gate_voltage=(max_in_voltage, max_in_voltage), # allow all - )) - self.amp = self.Block(Opamp()) - self.range = self.Block(ResistorMux([ - 1*kOhm(tol=0.01), # 1 mA range - 10*kOhm(tol=0.01), # 100 uA range - 100*kOhm(tol=0.01), # 10 uA range - 1*MOhm(tol=0.01), # 1 uA range (for MOhm measurements) - ])) - self.sw = self.Block(AnalogMuxer()) - - self.diode = self.Block(Diode( - reverse_voltage=self.voltage_rating, # protect against positive overvoltage - current=(0, max_in_voltage / 1000), # approx lowest resistance - TODO properly model the resistor mux - voltage_drop=(0, 1)*Volt, # TODO kind of arbitrary - reverse_recovery_time=RangeExpr.ALL - )) - - # this is connected in HDL (instead of schematic) because it needs a type conversion (from array[1] to element) - self.connect(self.sw.control.request(), self.enable) - - self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'fet.S': AnalogSink(), - 'fet.G': AnalogSink(), - 'range.com': VoltageSink( - current_draw=(0, max_in_voltage / 1000) # approx lowest resistance - TODO properly model the resistor mux - ), - 'range.sw': AnalogSource( - voltage_out=(0, max_in_voltage), - signal_out=(0, max_in_voltage), - impedance=(1, 1000)*kOhm # TODO properly model resistor mux - ), - 'output': AnalogSink( # TODO should be analog source - voltage_limits=self.voltage_rating + """Protected constant-current stage for the multimeter driver.""" + + def __init__(self, voltage_rating: RangeLike = RangeExpr()): + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.output = self.Port(AnalogSink.empty()) # TBD this should be some kind of AnalogBidirectional + + self.control = self.Port(AnalogSink.empty()) + self.select = self.Port(Vector(DigitalSink.empty())) + self.enable = self.Port(DigitalSink.empty()) + + self.voltage_rating = self.ArgParameter(voltage_rating) + + @override + def contents(self) -> None: + super().contents() + max_in_voltage = self.control.link().voltage.upper() + + self.fet = self.Block( + Fet.PFet( + drain_voltage=self.voltage_rating, # protect against negative overvoltage + drain_current=( + 0, + max_in_voltage / 1000, + ), # approx lowest resistance - TODO properly model the resistor mux + gate_voltage=(max_in_voltage, max_in_voltage), # allow all + ) + ) + self.amp = self.Block(Opamp()) + self.range = self.Block( + ResistorMux( + [ + 1 * kOhm(tol=0.01), # 1 mA range + 10 * kOhm(tol=0.01), # 100 uA range + 100 * kOhm(tol=0.01), # 10 uA range + 1 * MOhm(tol=0.01), # 1 uA range (for MOhm measurements) + ] + ) + ) + self.sw = self.Block(AnalogMuxer()) + + self.diode = self.Block( + Diode( + reverse_voltage=self.voltage_rating, # protect against positive overvoltage + current=(0, max_in_voltage / 1000), # approx lowest resistance - TODO properly model the resistor mux + voltage_drop=(0, 1) * Volt, # TODO kind of arbitrary + reverse_recovery_time=RangeExpr.ALL, + ) + ) + + # this is connected in HDL (instead of schematic) because it needs a type conversion (from array[1] to element) + self.connect(self.sw.control.request(), self.enable) + + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "fet.S": AnalogSink(), + "fet.G": AnalogSink(), + "range.com": VoltageSink( + current_draw=( + 0, + max_in_voltage / 1000, + ) # approx lowest resistance - TODO properly model the resistor mux + ), + "range.sw": AnalogSource( + voltage_out=(0, max_in_voltage), + signal_out=(0, max_in_voltage), + impedance=(1, 1000) * kOhm, # TODO properly model resistor mux + ), + "output": AnalogSink(voltage_limits=self.voltage_rating), # TODO should be analog source + }, ) - }) class Multimeter(JlcBoardTop): - """A BLE multimeter with volts/ohms/diode mode - everything but the curent mode. - Basically an ADC and programmable constant current driver with ranging circuits. - Good up to the specified VOLTAGE_RATING, in any measurement mode. - - IMPORTANT: HIGH VOLTAGE SAFETY ALSO DEPENDS ON MECHANICAL DESIGN AND LAYOUT. - NOT RECOMMENDED FOR USAGE ON HIGH VOLTAGES. - IMPORTANT: THERE IS NO INPUT OVERLOAD PROTECTION. - DO NOT PLUG INTO MAINS, WHERE VERY HIGH VOLTAGE TRANSIENTS (kV level) ARE POSSIBLE. - IMPORTANT: THE USB PORT IS NOT ELECTRICALLY ISOLATED. DO NOT MEASURE NON-ISOLATED - CIRCUITS WHILE USB IS PLUGGED IN. BE AWARE OF GROUND PATHS. - """ - - @override - def contents(self) -> None: - super().contents() - VOLTAGE_RATING = (0, 250) * Volt - - # also support LiIon AA batteries - self.bat = self.Block(AaBattery(voltage=(1.1, 4.2) * Volt, actual_voltage=(1.1, 4.2) * Volt)) - - # Data-only USB port, for example to connect to a computer that can't source USB PD - # so the PD port can be connected to a dedicated power brick. - self.data_usb = self.Block(UsbCReceptacle()) - - self.gnd = self.connect(self.bat.gnd, self.data_usb.gnd) - self.vbat = self.connect(self.bat.pwr) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.gate, self.reg_5v, self.tp_5v, self.prot_5v, - self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.vbat, - imp.Block(SoftPowerSwitch()), - imp.Block(BoostConverter(output_voltage=(4.5, 5.2)*Volt)), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(5.2, 6.5)*Volt)), - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v5v = self.connect(self.reg_5v.pwr_out) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - (self.reg_analog, self.tp_analog, self.prot_analog), _ = self.chain( - self.v5v, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.vanalog = self.connect(self.reg_analog.pwr_out) - - # DIGITAL DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(Mdbt50q_1mv2()) # needed to define required Vusb - # TODO ideally this would have a Ble mixin, but mixins can't be applied to the concrete microcontroller - - (self.vbatsense, ), _ = self.chain(self.gate.pwr_out, # TODO update to use VoltageSenseDivider - imp.Block(VoltageDivider(output_voltage=(0.6, 3)*Volt, impedance=(100, 1000)*Ohm)), - self.mcu.adc.request('vbatsense')) - - (self.usb_esd, ), _ = self.chain(self.data_usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) - self.connect(self.mcu.pwr_usb, self.data_usb.pwr) - - self.chain(self.gate.btn_out, self.mcu.gpio.request('sw0')) - self.chain(self.mcu.gpio.request('gate_control'), self.gate.control) - - self.rgb = imp.Block(IndicatorSinkRgbLed()) - self.connect(self.mcu.gpio.request_vector('rgb'), self.rgb.signals) - - (self.sw1, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw1')) - (self.sw2, ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw2')) - - lcd_spi = self.mcu.spi.request('lcd_spi') - self.lcd = imp.Block(Qt096t_if09()) - self.connect(self.reg_3v3.pwr_out.as_digital_source(), self.lcd.led) - self.connect(self.mcu.gpio.request('lcd_reset'), self.lcd.reset) - self.connect(self.mcu.gpio.request('lcd_rs'), self.lcd.rs) - self.connect(lcd_spi, self.lcd.spi) # MISO unused - self.connect(self.mcu.gpio.request('lcd_cs'), self.lcd.cs) - - # SPEAKER DOMAIN - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.spk_dac, self.spk_tp, self.spk_drv, self.spk), self.spk_chain = self.chain( - self.mcu.gpio.request('spk'), - imp.Block(LowPassRcDac(1*kOhm(tol=0.05), 5*kHertz(tol=0.5))), - self.Block(AnalogTestPoint()), - imp.Block(Tpa2005d1(gain=Range.from_tolerance(10, 0.2))), - self.Block(Speaker())) - - # the AA battery is incapable of driving this at full power, - # so this indicates it will be run at only partial power - (self.spk_pwr, ), _ = self.chain( - self.v5v, - self.Block(ForcedVoltageCurrentDraw((0, 0.05)*Amp)), - self.spk_drv.pwr - ) - - # ANALOG DOMAIN - with self.implicit_connect( - ImplicitConnect(self.vanalog, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.ref_div, self.ref_buf), _ = self.chain( - self.vanalog, - imp.Block(VoltageDivider(output_voltage=1.62*Volt(tol=0.05), impedance=(10, 100)*kOhm)), - imp.Block(OpampFollower()) - ) - self.vcenter = self.connect(self.ref_buf.output) - - # NEGATIVE PORT - # 'virtual ground' can be switched between GND (low impedance for the current driver) - # and Vdd/2 (high impedance, but can measure negative voltages) - self.inn = self.Block(BananaSafetyJack()) - self.inn_mux = imp.Block(AnalogMuxer()).mux_to( - inputs=[self.bat.gnd.as_analog_source(), self.ref_buf.output] - ) - self.inn_merge = self.Block(MergedAnalogSource()).connected_from( - self.inn_mux.out, self.inn.port.adapt_to(AnalogSource())) - - self.connect(self.mcu.gpio.request_vector('inn_control'), self.inn_mux.control) - - # POSITIVE PORT - self.inp = self.Block(BananaSafetyJack()) - inp_port = self.inp.port.adapt_to(AnalogSource( - voltage_out=VOLTAGE_RATING, - signal_out=VOLTAGE_RATING, - current_limits=(0, 10)*mAmp, - impedance=(0, 100)*Ohm, - )) - - # MEASUREMENT / SIGNAL CONDITIONING CIRCUITS - adc_spi = self.mcu.spi.request('adc_spi') - self.measure = imp.Block(MultimeterAnalog()) - self.connect(self.measure.input_positive, inp_port) - self.connect(self.measure.input_negative, self.inn_merge.output) - (self.measure_buffer, self.tp_measure), self.meas_chain = self.chain( - self.measure.output, - imp.Block(OpampFollower()), - self.Block(AnalogTestPoint())) - (self.adc, ), _ = self.chain( - imp.Block(Mcp3561()), - adc_spi) - self.connect(self.adc.pwr, self.v3v3) - self.connect(self.adc.pwra, self.vanalog) - self.connect(self.adc.vins.request('0'), self.measure_buffer.output) - self.connect(self.adc.vins.request('1'), self.inn_merge.output) - self.connect(self.mcu.gpio.request_vector('measure_select'), self.measure.select) - self.connect(self.mcu.gpio.request('adc_cs'), self.adc.cs) - - # DRIVER CIRCUITS - self.driver = imp.Block(MultimeterCurrentDriver( - voltage_rating=VOLTAGE_RATING - )) - self.connect(self.driver.output, inp_port) - (self.driver_dac, ), _ = self.chain( - self.mcu.gpio.request('driver_control'), - imp.Block(LowPassRcDac(1*kOhm(tol=0.05), 100*Hertz(tol=0.5))), - self.driver.control) - self.connect(self.mcu.gpio.request_vector('driver_select'), self.driver.select) - self.connect(self.mcu.gpio.request('driver_enable'), self.driver.enable) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['reg_5v'], Xc9142), - (['reg_3v3'], Lp5907), # could be a cheaper LDO actually - (['reg_analog'], Lp5907), - (['measure', 'range', 'switch'], AnalogSwitchTree), - (['driver', 'range', 'switch'], AnalogSwitchTree), - (['measure', 'res'], GenericChipResistor), - (['spk', 'conn'], JstPhKVertical), - - (['driver', 'fet'], CustomFet), - (['driver', 'diode'], CustomDiode), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'adc_spi.miso=24', - 'adc_spi.mosi=26', - 'adc_spi.sck=37', - 'adc_cs=39', - 'inn_control_0=41', - 'measure_select_0_0=42', - 'measure_select_1_0=43', - 'driver_select_1_0=44', - 'driver_select_0_0=46', - 'driver_enable=48', - 'gate_control=49', - 'sw0=50', - 'driver_control=45', # high frequency PWM - - 'sw1=16', - 'lcd_cs=17', - 'lcd_spi.sck=18', - 'lcd_spi.mosi=19', - 'lcd_spi.miso=NC', - 'lcd_rs=10', - 'lcd_reset=8', - 'sw2=3', - - 'spk=36', - 'vbatsense=9', - - 'rgb_blue=6', - 'rgb_red=4', - 'rgb_green=5', - ]), - (['mcu', 'swd_swo_pin'], 'P1.00'), - (['reg_5v', 'power_path', 'dutycycle_limit'], Range(float('-inf'), float('inf'))), # allow the regulator to go into tracking mode - (['reg_5v', 'fb', 'div', 'series'], 12), # JLC has limited resistors - (['measure', 'res', 'footprint_spec'], 'Resistor_SMD:R_2512_6332Metric'), # beefy input resistor - (['measure', 'res', 'fp_mfr'], 'Bourns Inc.'), - (['measure', 'res', 'fp_part'], 'CHV2512-F*-1004***'), - # IMPORTANT! Most 2512 resistors are rated to ~200V working voltage, this one is up to 3kV. - - # pin footprints to re-select parts with newer parts tables - (['driver', 'fet', 'footprint_spec'], 'Package_TO_SOT_SMD:SOT-23'), # Q3 - (['driver', 'fet', 'manufacturer_spec'], 'Infineon Technologies'), - (['driver', 'fet', 'part_spec'], 'BSR92PH6327XTSA1'), - (['driver', 'diode', 'footprint_spec'], 'Diode_SMD:D_SMA'), - (['driver', 'diode', 'manufacturer_spec'], 'Micro Commercial Co'), - (['driver', 'diode', 'part_spec'], 'GS1G-LTP'), - # (['reg_5v', 'power_path', 'inductor', 'footprint_spec'], 'Inductor_SMD:L_0805_2012Metric'), # L1 - - # JLC does not have frequency specs, must be checked TODO - (['reg_5v', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - ], - class_values=[ - (AnalogSwitchTree, ['switch_size'], 2), - ], - class_refinements=[ - (SwdCortexTargetConnector, SwdCortexTargetTc2050), - (TagConnect, TagConnectNonLegged), - (Opamp, Tlv9061), # higher precision opamps - (BananaSafetyJack, Fcr7350), - (AnalogSwitch, Nlas4157), - (Speaker, ConnectorSpeaker), - ], - ) + """A BLE multimeter with volts/ohms/diode mode - everything but the curent mode. + Basically an ADC and programmable constant current driver with ranging circuits. + Good up to the specified VOLTAGE_RATING, in any measurement mode. + + IMPORTANT: HIGH VOLTAGE SAFETY ALSO DEPENDS ON MECHANICAL DESIGN AND LAYOUT. + NOT RECOMMENDED FOR USAGE ON HIGH VOLTAGES. + IMPORTANT: THERE IS NO INPUT OVERLOAD PROTECTION. + DO NOT PLUG INTO MAINS, WHERE VERY HIGH VOLTAGE TRANSIENTS (kV level) ARE POSSIBLE. + IMPORTANT: THE USB PORT IS NOT ELECTRICALLY ISOLATED. DO NOT MEASURE NON-ISOLATED + CIRCUITS WHILE USB IS PLUGGED IN. BE AWARE OF GROUND PATHS. + """ + + @override + def contents(self) -> None: + super().contents() + VOLTAGE_RATING = (0, 250) * Volt + + # also support LiIon AA batteries + self.bat = self.Block(AaBattery(voltage=(1.1, 4.2) * Volt, actual_voltage=(1.1, 4.2) * Volt)) + + # Data-only USB port, for example to connect to a computer that can't source USB PD + # so the PD port can be connected to a dedicated power brick. + self.data_usb = self.Block(UsbCReceptacle()) + + self.gnd = self.connect(self.bat.gnd, self.data_usb.gnd) + self.vbat = self.connect(self.bat.pwr) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.gate, self.reg_5v, self.tp_5v, self.prot_5v, self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = ( + self.chain( + self.vbat, + imp.Block(SoftPowerSwitch()), + imp.Block(BoostConverter(output_voltage=(4.5, 5.2) * Volt)), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(5.2, 6.5) * Volt)), + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + ) + self.v5v = self.connect(self.reg_5v.pwr_out) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + (self.reg_analog, self.tp_analog, self.prot_analog), _ = self.chain( + self.v5v, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.vanalog = self.connect(self.reg_analog.pwr_out) + + # DIGITAL DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(Mdbt50q_1mv2()) # needed to define required Vusb + # TODO ideally this would have a Ble mixin, but mixins can't be applied to the concrete microcontroller + + (self.vbatsense,), _ = self.chain( + self.gate.pwr_out, # TODO update to use VoltageSenseDivider + imp.Block(VoltageDivider(output_voltage=(0.6, 3) * Volt, impedance=(100, 1000) * Ohm)), + self.mcu.adc.request("vbatsense"), + ) + + (self.usb_esd,), _ = self.chain(self.data_usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + self.connect(self.mcu.pwr_usb, self.data_usb.pwr) + + self.chain(self.gate.btn_out, self.mcu.gpio.request("sw0")) + self.chain(self.mcu.gpio.request("gate_control"), self.gate.control) + + self.rgb = imp.Block(IndicatorSinkRgbLed()) + self.connect(self.mcu.gpio.request_vector("rgb"), self.rgb.signals) + + (self.sw1,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw1")) + (self.sw2,), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw2")) + + lcd_spi = self.mcu.spi.request("lcd_spi") + self.lcd = imp.Block(Qt096t_if09()) + self.connect(self.reg_3v3.pwr_out.as_digital_source(), self.lcd.led) + self.connect(self.mcu.gpio.request("lcd_reset"), self.lcd.reset) + self.connect(self.mcu.gpio.request("lcd_rs"), self.lcd.rs) + self.connect(lcd_spi, self.lcd.spi) # MISO unused + self.connect(self.mcu.gpio.request("lcd_cs"), self.lcd.cs) + + # SPEAKER DOMAIN + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.spk_dac, self.spk_tp, self.spk_drv, self.spk), self.spk_chain = self.chain( + self.mcu.gpio.request("spk"), + imp.Block(LowPassRcDac(1 * kOhm(tol=0.05), 5 * kHertz(tol=0.5))), + self.Block(AnalogTestPoint()), + imp.Block(Tpa2005d1(gain=Range.from_tolerance(10, 0.2))), + self.Block(Speaker()), + ) + + # the AA battery is incapable of driving this at full power, + # so this indicates it will be run at only partial power + (self.spk_pwr,), _ = self.chain( + self.v5v, self.Block(ForcedVoltageCurrentDraw((0, 0.05) * Amp)), self.spk_drv.pwr + ) + + # ANALOG DOMAIN + with self.implicit_connect( + ImplicitConnect(self.vanalog, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.ref_div, self.ref_buf), _ = self.chain( + self.vanalog, + imp.Block(VoltageDivider(output_voltage=1.62 * Volt(tol=0.05), impedance=(10, 100) * kOhm)), + imp.Block(OpampFollower()), + ) + self.vcenter = self.connect(self.ref_buf.output) + + # NEGATIVE PORT + # 'virtual ground' can be switched between GND (low impedance for the current driver) + # and Vdd/2 (high impedance, but can measure negative voltages) + self.inn = self.Block(BananaSafetyJack()) + self.inn_mux = imp.Block(AnalogMuxer()).mux_to( + inputs=[self.bat.gnd.as_analog_source(), self.ref_buf.output] + ) + self.inn_merge = self.Block(MergedAnalogSource()).connected_from( + self.inn_mux.out, self.inn.port.adapt_to(AnalogSource()) + ) + + self.connect(self.mcu.gpio.request_vector("inn_control"), self.inn_mux.control) + + # POSITIVE PORT + self.inp = self.Block(BananaSafetyJack()) + inp_port = self.inp.port.adapt_to( + AnalogSource( + voltage_out=VOLTAGE_RATING, + signal_out=VOLTAGE_RATING, + current_limits=(0, 10) * mAmp, + impedance=(0, 100) * Ohm, + ) + ) + + # MEASUREMENT / SIGNAL CONDITIONING CIRCUITS + adc_spi = self.mcu.spi.request("adc_spi") + self.measure = imp.Block(MultimeterAnalog()) + self.connect(self.measure.input_positive, inp_port) + self.connect(self.measure.input_negative, self.inn_merge.output) + (self.measure_buffer, self.tp_measure), self.meas_chain = self.chain( + self.measure.output, imp.Block(OpampFollower()), self.Block(AnalogTestPoint()) + ) + (self.adc,), _ = self.chain(imp.Block(Mcp3561()), adc_spi) + self.connect(self.adc.pwr, self.v3v3) + self.connect(self.adc.pwra, self.vanalog) + self.connect(self.adc.vins.request("0"), self.measure_buffer.output) + self.connect(self.adc.vins.request("1"), self.inn_merge.output) + self.connect(self.mcu.gpio.request_vector("measure_select"), self.measure.select) + self.connect(self.mcu.gpio.request("adc_cs"), self.adc.cs) + + # DRIVER CIRCUITS + self.driver = imp.Block(MultimeterCurrentDriver(voltage_rating=VOLTAGE_RATING)) + self.connect(self.driver.output, inp_port) + (self.driver_dac,), _ = self.chain( + self.mcu.gpio.request("driver_control"), + imp.Block(LowPassRcDac(1 * kOhm(tol=0.05), 100 * Hertz(tol=0.5))), + self.driver.control, + ) + self.connect(self.mcu.gpio.request_vector("driver_select"), self.driver.select) + self.connect(self.mcu.gpio.request("driver_enable"), self.driver.enable) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["reg_5v"], Xc9142), + (["reg_3v3"], Lp5907), # could be a cheaper LDO actually + (["reg_analog"], Lp5907), + (["measure", "range", "switch"], AnalogSwitchTree), + (["driver", "range", "switch"], AnalogSwitchTree), + (["measure", "res"], GenericChipResistor), + (["spk", "conn"], JstPhKVertical), + (["driver", "fet"], CustomFet), + (["driver", "diode"], CustomDiode), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "adc_spi.miso=24", + "adc_spi.mosi=26", + "adc_spi.sck=37", + "adc_cs=39", + "inn_control_0=41", + "measure_select_0_0=42", + "measure_select_1_0=43", + "driver_select_1_0=44", + "driver_select_0_0=46", + "driver_enable=48", + "gate_control=49", + "sw0=50", + "driver_control=45", # high frequency PWM + "sw1=16", + "lcd_cs=17", + "lcd_spi.sck=18", + "lcd_spi.mosi=19", + "lcd_spi.miso=NC", + "lcd_rs=10", + "lcd_reset=8", + "sw2=3", + "spk=36", + "vbatsense=9", + "rgb_blue=6", + "rgb_red=4", + "rgb_green=5", + ], + ), + (["mcu", "swd_swo_pin"], "P1.00"), + ( + ["reg_5v", "power_path", "dutycycle_limit"], + Range(float("-inf"), float("inf")), + ), # allow the regulator to go into tracking mode + (["reg_5v", "fb", "div", "series"], 12), # JLC has limited resistors + (["measure", "res", "footprint_spec"], "Resistor_SMD:R_2512_6332Metric"), # beefy input resistor + (["measure", "res", "fp_mfr"], "Bourns Inc."), + (["measure", "res", "fp_part"], "CHV2512-F*-1004***"), + # IMPORTANT! Most 2512 resistors are rated to ~200V working voltage, this one is up to 3kV. + # pin footprints to re-select parts with newer parts tables + (["driver", "fet", "footprint_spec"], "Package_TO_SOT_SMD:SOT-23"), # Q3 + (["driver", "fet", "manufacturer_spec"], "Infineon Technologies"), + (["driver", "fet", "part_spec"], "BSR92PH6327XTSA1"), + (["driver", "diode", "footprint_spec"], "Diode_SMD:D_SMA"), + (["driver", "diode", "manufacturer_spec"], "Micro Commercial Co"), + (["driver", "diode", "part_spec"], "GS1G-LTP"), + # (['reg_5v', 'power_path', 'inductor', 'footprint_spec'], 'Inductor_SMD:L_0805_2012Metric'), # L1 + # JLC does not have frequency specs, must be checked TODO + (["reg_5v", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + ], + class_values=[ + (AnalogSwitchTree, ["switch_size"], 2), + ], + class_refinements=[ + (SwdCortexTargetConnector, SwdCortexTargetTc2050), + (TagConnect, TagConnectNonLegged), + (Opamp, Tlv9061), # higher precision opamps + (BananaSafetyJack, Fcr7350), + (AnalogSwitch, Nlas4157), + (Speaker, ConnectorSpeaker), + ], + ) class MultimeterTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(Multimeter) + def test_design(self) -> None: + compile_board_inplace(Multimeter) diff --git a/examples/test_pcbbot.py b/examples/test_pcbbot.py index 14df1e820..406b181c4 100644 --- a/examples/test_pcbbot.py +++ b/examples/test_pcbbot.py @@ -8,229 +8,223 @@ class PcbBot(JlcBoardTop): - """Robot driver that uses a ESP32 w/ camera and has student-proofing - - Key features: - - USB-C receptacle for power input and battery charging. - - LiPo battery connector with voltage regulation and protection circuits. - - Power management with priority power selection, fuse protection, and gate control. - - 3.3V voltage regulation for the main logic level power supply. - - Integrated battery charging circuit with status indication. - - I2C communication interface with pull-up resistors and test points. - - Time-of-flight (ToF) sensor array for distance measurement. - - Inertial Measurement Unit (IMU) and magnetometer for orientation sensing. - - IO expander for additional GPIOs and a thru-hole RGB LED indicator. - - OLED display - - PWM Servo motor - - Neopixel LED array for RGB lighting - - Camera module with voltage regulation for image capture. - - Digital switch for power control. - - Keyboard mechanical switch with RGB LED - - Ducky touch button - - Known issues: - - Charging ic is not reverse protected - - MCU does not get turned off with the gate when powered by the USB and battery, though the vbatt line turns off. (by design) - """ - @override - def contents(self) -> None: - super().contents() - - self.usb = self.Block(UsbCReceptacle(current_limits=(0, 3)*Amp)) - self.vusb = self.connect(self.usb.pwr) - - self.batt = self.Block(LipoConnector(actual_voltage=(3.7, 4.2)*Volt)) - - self.gnd = self.connect(self.usb.gnd, self.batt.gnd) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.fuse, self.gate, self.prot_batt, self.tp_batt), _ = self.chain( - self.batt.pwr, - imp.Block(SeriesPowerFuse((2, 4)*Amp)), - imp.Block(SoftPowerSwitch()), - imp.Block(ProtectionZenerDiode(voltage=(4.5, 6.0)*Volt)), - self.Block(VoltageTestPoint())) - self.vbatt = self.connect(self.gate.pwr_out) # downstream of fuse - - self.pwr_or = self.Block(PriorityPowerOr( # also does reverse protection - (0, 1)*Volt, (0, 0.1)*Ohm - )).connected_from(self.usb.gnd, self.usb.pwr, self.gate.pwr_out) - self.pwr = self.connect(self.pwr_or.pwr_out) - - (self.reg_3v3, self.prot_3v3, self.tp_3v3), _ = self.chain( - self.pwr, - imp.Block(VoltageRegulator(output_voltage=3.3*Volt(tol=0.05))), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)), - self.Block(VoltageTestPoint()), - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - (self.charger, ), _ = self.chain( - self.vusb, imp.Block(Mcp73831(200*mAmp(tol=0.2))), self.batt.chg - ) - (self.charge_led, ), _ = self.chain( - self.Block(IndicatorSinkLed(Led.Yellow)), self.charger.stat - ) - self.connect(self.vusb, self.charge_led.pwr) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) - - # single onboard debugging LED - (self.led, ), _ = self.chain(self.mcu.gpio.request('led'), imp.Block(IndicatorLed(Led.Red))) - - (self.touch_sink, ), self.touch = self.chain(self.mcu.gpio.request('touch'), imp.Block(DummyDigitalSink())) - - self.i2c = self.mcu.i2c.request('i2c') - - self.tof = imp.Block(Vl53l0xArray(4, first_reset_fixed=True)) - (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( - self.i2c, - imp.Block(I2cPullup()), imp.Block(I2cTestPoint('i2c')), - self.tof.i2c) - - # IMU - self.imu = imp.Block(Lsm6ds3trc()) - self.mag = imp.Block(Qmc5883l()) - self.connect(self.i2c, self.imu.i2c, self.mag.i2c) - - # IO EXPANDER - self.expander = imp.Block(Pca9554()) - self.connect(self.i2c, self.expander.i2c) - - self.connect(self.expander.io.request_vector('tof_reset'), self.tof.reset) - - (self.rgb, ), _ = self.chain(self.expander.io.request_vector('rgb'), imp.Block(IndicatorSinkRgbLed())) - - self.oled = imp.Block(Er_Oled_096_1_1()) - self.connect(self.i2c, self.oled.i2c) - self.connect(self.oled.reset, self.mcu.gpio.request("oled_reset")) - - (self.batt_sense, ), _ = self.chain( - self.vbatt, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('vbatt_sense') - ) - - self.chain(self.gate.btn_out, self.mcu.gpio.request('sw0')) - self.chain(self.mcu.gpio.request('gate_control'), self.gate.control) - - # VBATT DOMAIN - with self.implicit_connect( - ImplicitConnect(self.vbatt, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.servo = ElementDict[PwmConnector]() - for i in range(4): - servo = self.servo[i] = imp.Block(PwmConnector((0, 200)*mAmp)) - self.connect(self.mcu.gpio.request(f'servo{i}'), servo.pwm) - - - (self.npx, self.npx_key), _ = self.chain(self.mcu.gpio.request('npx'), imp.Block(NeopixelArray(4*4)), imp.Block(Neopixel())) - - - # CAMERA MULTI DOMAIN - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_2v5, ), _ = self.chain(self.pwr, imp.Block(VoltageRegulator(output_voltage=2.5*Volt(tol=0.05)))) - self.v2v5 = self.connect(self.reg_2v5.pwr_out) - (self.reg_1v2, ), _ = self.chain(self.pwr, imp.Block(VoltageRegulator(output_voltage=1.2*Volt(tol=0.05)))) - self.v1v2 = self.connect(self.reg_1v2.pwr_out) - - self.cam = imp.Block(Ov2640_Fpc24()) - self.connect(self.cam.pwr, self.v3v3) - self.connect(self.cam.pwr_analog, self.v2v5) - self.connect(self.cam.pwr_digital, self.v1v2) - self.connect(self.mcu.with_mixin(IoControllerDvp8()).dvp8.request('cam'), self.cam.dvp8) - self.connect(self.cam.sio, self.i2c) - - with self.implicit_connect( + """Robot driver that uses a ESP32 w/ camera and has student-proofing + + Key features: + - USB-C receptacle for power input and battery charging. + - LiPo battery connector with voltage regulation and protection circuits. + - Power management with priority power selection, fuse protection, and gate control. + - 3.3V voltage regulation for the main logic level power supply. + - Integrated battery charging circuit with status indication. + - I2C communication interface with pull-up resistors and test points. + - Time-of-flight (ToF) sensor array for distance measurement. + - Inertial Measurement Unit (IMU) and magnetometer for orientation sensing. + - IO expander for additional GPIOs and a thru-hole RGB LED indicator. + - OLED display + - PWM Servo motor + - Neopixel LED array for RGB lighting + - Camera module with voltage regulation for image capture. + - Digital switch for power control. + - Keyboard mechanical switch with RGB LED + - Ducky touch button + + Known issues: + - Charging ic is not reverse protected + - MCU does not get turned off with the gate when powered by the USB and battery, though the vbatt line turns off. (by design) + """ + + @override + def contents(self) -> None: + super().contents() + + self.usb = self.Block(UsbCReceptacle(current_limits=(0, 3) * Amp)) + self.vusb = self.connect(self.usb.pwr) + + self.batt = self.Block(LipoConnector(actual_voltage=(3.7, 4.2) * Volt)) + + self.gnd = self.connect(self.usb.gnd, self.batt.gnd) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + + # POWER + with self.implicit_connect( ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.switch = imp.Block(DigitalSwitch()) - self.connect(self.switch.out, self.mcu.gpio.request('pwr')) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['reg_3v3'], Ldl1117), - (['tof', 'elt[0]', 'conn'], PinSocket254), - (['tof', 'elt[1]', 'conn'], PinSocket254), - (['tof', 'elt[2]', 'conn'], PinSocket254), - (['tof', 'elt[3]', 'conn'], PinSocket254), - (['switch', 'package'], KailhSocket), - - (['reg_2v5'], Xc6206p), - (['reg_1v2'], Xc6206p), - (['rgb', 'package'], ThtRgbLed), - (['npx_key'], Sk6812Mini_E), - (['fuse', 'fuse'], PptcFuse), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'cam.y2=25', - 'cam.y1=24', - 'cam.y3=23', - 'cam.y0=22', - 'cam.y4=21', - 'cam.pclk=20', - 'cam.y5=19', - 'cam.y6=18', - 'cam.xclk=17', - 'cam.y7=15', - 'cam.href=12', - 'cam.vsync=11', - - "i2c=I2CEXT0", - "i2c.scl=38", - "i2c.sda=4", - "0.dp=14", - "0.dm=13", - "servo0=5", - "servo1=6", - "servo2=8", - "servo3=10", - "npx=9", - 'led=_GPIO0_STRAP', - 'touch=GPIO7' - ]), - (['expander', 'pin_assigns'], [ - ]), - - (['prot_batt', 'diode', 'footprint_spec'], 'Diode_SMD:D_SMA'), # big diodes to dissipate more power - (['reg_3v3', 'ic', 'actual_dropout'], Range(0.0, 0.3)) # tighter dropout from lower current draw - ], - class_refinements=[ - (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified - (TestPoint, CompactKeystone5015), - (Vl53l0x, Vl53l0xConnector), - (Speaker, ConnectorSpeaker), - (Neopixel, Ws2812b), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - - # the camera recommended specs are excessively tight, so loosen them a bit - (Ov2640_Fpc24, ['device', 'avdd', 'voltage_limits'], Range(2.3, 3.0)), # allow 2v5 - (Nonstrict3v3Compatible, ['nonstrict_3v3_compatible'], True), - ], - ) + ) as imp: + (self.fuse, self.gate, self.prot_batt, self.tp_batt), _ = self.chain( + self.batt.pwr, + imp.Block(SeriesPowerFuse((2, 4) * Amp)), + imp.Block(SoftPowerSwitch()), + imp.Block(ProtectionZenerDiode(voltage=(4.5, 6.0) * Volt)), + self.Block(VoltageTestPoint()), + ) + self.vbatt = self.connect(self.gate.pwr_out) # downstream of fuse + + self.pwr_or = self.Block( + PriorityPowerOr((0, 1) * Volt, (0, 0.1) * Ohm) # also does reverse protection + ).connected_from(self.usb.gnd, self.usb.pwr, self.gate.pwr_out) + self.pwr = self.connect(self.pwr_or.pwr_out) + + (self.reg_3v3, self.prot_3v3, self.tp_3v3), _ = self.chain( + self.pwr, + imp.Block(VoltageRegulator(output_voltage=3.3 * Volt(tol=0.05))), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + self.Block(VoltageTestPoint()), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + (self.charger,), _ = self.chain(self.vusb, imp.Block(Mcp73831(200 * mAmp(tol=0.2))), self.batt.chg) + (self.charge_led,), _ = self.chain(self.Block(IndicatorSinkLed(Led.Yellow)), self.charger.stat) + self.connect(self.vusb, self.charge_led.pwr) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + # single onboard debugging LED + (self.led,), _ = self.chain(self.mcu.gpio.request("led"), imp.Block(IndicatorLed(Led.Red))) + + (self.touch_sink,), self.touch = self.chain(self.mcu.gpio.request("touch"), imp.Block(DummyDigitalSink())) + + self.i2c = self.mcu.i2c.request("i2c") + + self.tof = imp.Block(Vl53l0xArray(4, first_reset_fixed=True)) + (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( + self.i2c, imp.Block(I2cPullup()), imp.Block(I2cTestPoint("i2c")), self.tof.i2c + ) + + # IMU + self.imu = imp.Block(Lsm6ds3trc()) + self.mag = imp.Block(Qmc5883l()) + self.connect(self.i2c, self.imu.i2c, self.mag.i2c) + + # IO EXPANDER + self.expander = imp.Block(Pca9554()) + self.connect(self.i2c, self.expander.i2c) + + self.connect(self.expander.io.request_vector("tof_reset"), self.tof.reset) + + (self.rgb,), _ = self.chain(self.expander.io.request_vector("rgb"), imp.Block(IndicatorSinkRgbLed())) + + self.oled = imp.Block(Er_Oled_096_1_1()) + self.connect(self.i2c, self.oled.i2c) + self.connect(self.oled.reset, self.mcu.gpio.request("oled_reset")) + + (self.batt_sense,), _ = self.chain( + self.vbatt, + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("vbatt_sense"), + ) + + self.chain(self.gate.btn_out, self.mcu.gpio.request("sw0")) + self.chain(self.mcu.gpio.request("gate_control"), self.gate.control) + + # VBATT DOMAIN + with self.implicit_connect( + ImplicitConnect(self.vbatt, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.servo = ElementDict[PwmConnector]() + for i in range(4): + servo = self.servo[i] = imp.Block(PwmConnector((0, 200) * mAmp)) + self.connect(self.mcu.gpio.request(f"servo{i}"), servo.pwm) + + (self.npx, self.npx_key), _ = self.chain( + self.mcu.gpio.request("npx"), imp.Block(NeopixelArray(4 * 4)), imp.Block(Neopixel()) + ) + + # CAMERA MULTI DOMAIN + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_2v5,), _ = self.chain(self.pwr, imp.Block(VoltageRegulator(output_voltage=2.5 * Volt(tol=0.05)))) + self.v2v5 = self.connect(self.reg_2v5.pwr_out) + (self.reg_1v2,), _ = self.chain(self.pwr, imp.Block(VoltageRegulator(output_voltage=1.2 * Volt(tol=0.05)))) + self.v1v2 = self.connect(self.reg_1v2.pwr_out) + + self.cam = imp.Block(Ov2640_Fpc24()) + self.connect(self.cam.pwr, self.v3v3) + self.connect(self.cam.pwr_analog, self.v2v5) + self.connect(self.cam.pwr_digital, self.v1v2) + self.connect(self.mcu.with_mixin(IoControllerDvp8()).dvp8.request("cam"), self.cam.dvp8) + self.connect(self.cam.sio, self.i2c) + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.switch = imp.Block(DigitalSwitch()) + self.connect(self.switch.out, self.mcu.gpio.request("pwr")) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["reg_3v3"], Ldl1117), + (["tof", "elt[0]", "conn"], PinSocket254), + (["tof", "elt[1]", "conn"], PinSocket254), + (["tof", "elt[2]", "conn"], PinSocket254), + (["tof", "elt[3]", "conn"], PinSocket254), + (["switch", "package"], KailhSocket), + (["reg_2v5"], Xc6206p), + (["reg_1v2"], Xc6206p), + (["rgb", "package"], ThtRgbLed), + (["npx_key"], Sk6812Mini_E), + (["fuse", "fuse"], PptcFuse), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "cam.y2=25", + "cam.y1=24", + "cam.y3=23", + "cam.y0=22", + "cam.y4=21", + "cam.pclk=20", + "cam.y5=19", + "cam.y6=18", + "cam.xclk=17", + "cam.y7=15", + "cam.href=12", + "cam.vsync=11", + "i2c=I2CEXT0", + "i2c.scl=38", + "i2c.sda=4", + "0.dp=14", + "0.dm=13", + "servo0=5", + "servo1=6", + "servo2=8", + "servo3=10", + "npx=9", + "led=_GPIO0_STRAP", + "touch=GPIO7", + ], + ), + (["expander", "pin_assigns"], []), + (["prot_batt", "diode", "footprint_spec"], "Diode_SMD:D_SMA"), # big diodes to dissipate more power + (["reg_3v3", "ic", "actual_dropout"], Range(0.0, 0.3)), # tighter dropout from lower current draw + ], + class_refinements=[ + (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified + (TestPoint, CompactKeystone5015), + (Vl53l0x, Vl53l0xConnector), + (Speaker, ConnectorSpeaker), + (Neopixel, Ws2812b), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + # the camera recommended specs are excessively tight, so loosen them a bit + (Ov2640_Fpc24, ["device", "avdd", "voltage_limits"], Range(2.3, 3.0)), # allow 2v5 + (Nonstrict3v3Compatible, ["nonstrict_3v3_compatible"], True), + ], + ) class PcbBotTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(PcbBot) + def test_design(self) -> None: + compile_board_inplace(PcbBot) diff --git a/examples/test_protected_charger.py b/examples/test_protected_charger.py index 80ccd7f52..3d2123897 100644 --- a/examples/test_protected_charger.py +++ b/examples/test_protected_charger.py @@ -26,38 +26,34 @@ def contents(self) -> None: self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.gnd, [Common]), ) as imp: self.tp = self.Block(VoltageTestPoint()).connected(self.batt.pwr) self.pmos = imp.Block(PmosChargerReverseProtection()) - (self.charger,), _ = self.chain( - self.vusb, imp.Block(Mcp73831(200 * mAmp(tol=0.2))), self.pmos.chg_in - ) + (self.charger,), _ = self.chain(self.vusb, imp.Block(Mcp73831(200 * mAmp(tol=0.2))), self.pmos.chg_in) self.connect(self.pmos.pwr_in, self.batt.pwr) self.connect(self.pmos.chg_out, self.batt.chg) - (self.charge_led,), _ = self.chain( - self.Block(IndicatorSinkLed(Led.Yellow)), self.charger.stat - ) + (self.charge_led,), _ = self.chain(self.Block(IndicatorSinkLed(Led.Yellow)), self.charger.stat) self.connect(self.vusb, self.charge_led.pwr) self.pmos_load = imp.Block(PmosReverseProtection(gate_resistor=300 * Ohm(tol=0.05))) self.pwr_pins = self.Block(PassiveConnector(length=3)) - self.connect(self.pwr_pins.pins.request('1').adapt_to(Ground()), self.gnd) + self.connect(self.pwr_pins.pins.request("1").adapt_to(Ground()), self.gnd) self.connect(self.pmos.pwr_out, self.pmos_load.pwr_in) - self.connect(self.pmos_load.pwr_out, - self.pwr_pins.pins.request('2').adapt_to(VoltageSink(current_draw=(0, 2.0) * Amp))) - self.connect(self.pwr_pins.pins.request('3').adapt_to(Ground()), self.gnd) + self.connect( + self.pmos_load.pwr_out, self.pwr_pins.pins.request("2").adapt_to(VoltageSink(current_draw=(0, 2.0) * Amp)) + ) + self.connect(self.pwr_pins.pins.request("3").adapt_to(Ground()), self.gnd) @override def refinements(self) -> Refinements: return super().refinements() + Refinements( - instance_refinements=[ - ], + instance_refinements=[], instance_values=[ - (['pmos', 'mp2', 'drain_current'], Range(0.0, 4.0)), + (["pmos", "mp2", "drain_current"], Range(0.0, 4.0)), ], class_refinements=[ (PassiveConnector, JstPhKHorizontal), # default connector series unless otherwise specified diff --git a/examples/test_robotcrawler.py b/examples/test_robotcrawler.py index c61f7edf6..93196dd12 100644 --- a/examples/test_robotcrawler.py +++ b/examples/test_robotcrawler.py @@ -6,274 +6,282 @@ class ServoFeedbackConnector(Connector, Block): - """4-pin connector modeling the FS90-FB micro servo with positional feedback, - https://www.pololu.com/product/3436 - """ - def __init__(self) -> None: - super().__init__() - self.conn = self.Block(PinHeader254(4)) - - self.pwm = self.Export(self.conn.pins.request('1').adapt_to(DigitalSink()), # no specs given - [Input]) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSink( - current_draw=(5, 800)*mAmp # idle @ 4.8v to stall @ 6v - )), [Power]) - self.gnd = self.Export(self.conn.pins.request('3').adapt_to(Ground()), - [Common]) - - self.fb = self.Export(self.conn.pins.request('4').adapt_to(AnalogSource( # no specs given - voltage_out=(0.9, 2.1)*Volt, # from https://www.pololu.com/blog/814/new-products-special-servos-with-position-feedback - signal_out=(0.9, 2.1)*Volt - ))) + """4-pin connector modeling the FS90-FB micro servo with positional feedback, + https://www.pololu.com/product/3436 + """ + + def __init__(self) -> None: + super().__init__() + self.conn = self.Block(PinHeader254(4)) + + self.pwm = self.Export(self.conn.pins.request("1").adapt_to(DigitalSink()), [Input]) # no specs given + self.pwr = self.Export( + self.conn.pins.request("2").adapt_to( + VoltageSink(current_draw=(5, 800) * mAmp) # idle @ 4.8v to stall @ 6v + ), + [Power], + ) + self.gnd = self.Export(self.conn.pins.request("3").adapt_to(Ground()), [Common]) + + self.fb = self.Export( + self.conn.pins.request("4").adapt_to( + AnalogSource( # no specs given + voltage_out=(0.9, 2.1) + * Volt, # from https://www.pololu.com/blog/814/new-products-special-servos-with-position-feedback + signal_out=(0.9, 2.1) * Volt, + ) + ) + ) @abstract_block class RobotCrawlerSpec(BoardTop): - """Example spec for a robot crawler, that defines the needed interface blocks but no connections - or infrastructure parts""" - SERVO_COUNT = 12 - SERVO_CAM_COUNT = 2 - def __init__(self) -> None: - super().__init__() - self.batt = self.Block(LipoConnector(actual_voltage=(3.7, 4.2)*Volt)) + """Example spec for a robot crawler, that defines the needed interface blocks but no connections + or infrastructure parts""" - self.servos = ElementDict[ServoFeedbackConnector]() - for i in range(self.SERVO_COUNT): - self.servos[str(i)] = self.Block(ServoFeedbackConnector()) - self.imu = self.Block(Lsm6ds3trc()) + SERVO_COUNT = 12 + SERVO_CAM_COUNT = 2 - self.servos_cam = ElementDict[ServoFeedbackConnector]() - for i in range(self.SERVO_CAM_COUNT): - self.servos_cam[str(i)] = self.Block(ServoFeedbackConnector()) + def __init__(self) -> None: + super().__init__() + self.batt = self.Block(LipoConnector(actual_voltage=(3.7, 4.2) * Volt)) + + self.servos = ElementDict[ServoFeedbackConnector]() + for i in range(self.SERVO_COUNT): + self.servos[str(i)] = self.Block(ServoFeedbackConnector()) + self.imu = self.Block(Lsm6ds3trc()) + + self.servos_cam = ElementDict[ServoFeedbackConnector]() + for i in range(self.SERVO_CAM_COUNT): + self.servos_cam[str(i)] = self.Block(ServoFeedbackConnector()) class RobotCrawler(RobotCrawlerSpec, JlcBoardTop): - """Implementation of the crawler robot, that implements what is needed to connect the interface blocks - as well as optional additional blocks. - """ - @override - def contents(self) -> None: - super().contents() - - self.vbatt = self.connect(self.batt.pwr) - self.gnd = self.connect(self.batt.gnd) - - self.tp_vbatt = self.Block(VoltageTestPoint()).connected(self.batt.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.batt.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3), _ = self.chain( - self.vbatt, - imp.Block(VoltageRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - (self.reg_14v, self.tp_14v), _ = self.chain( - self.vbatt, - imp.Block(VoltageRegulator(output_voltage=(13, 16)*Volt)), - self.Block(VoltageTestPoint()) - ) - self.v14 = self.connect(self.reg_14v.pwr_out) - - (self.reg_2v5, ), _ = self.chain( - self.vbatt, - imp.Block(VoltageRegulator(output_voltage=2.5*Volt(tol=0.05))) - ) - self.v2v5 = self.connect(self.reg_2v5.pwr_out) - - (self.reg_1v2, ), _ = self.chain( - self.vbatt, - imp.Block(VoltageRegulator(output_voltage=1.2*Volt(tol=0.05))) - ) - self.v1v2 = self.connect(self.reg_1v2.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu_servo = imp.Block(IoController()) - self.connect(self.mcu.gpio.request('srv_rst'), self.mcu_servo.with_mixin(Resettable()).reset) - self.mcu_test = imp.Block(IoController()) # test revised subcircuit only - - self.i2c = self.mcu.i2c.request('i2c') - (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( - self.i2c, - imp.Block(I2cPullup()), imp.Block(I2cTestPoint())) - self.connect(self.i2c, self.imu.i2c, - self.mcu_servo.with_mixin(IoControllerI2cTarget()).i2c_target.request('i2c'), - self.mcu_test.with_mixin(IoControllerI2cTarget()).i2c_target.request('i2c')) - - self.connect(self.v3v3, self.imu.pwr) - self.connect(self.gnd, self.imu.gnd) - - (self.led, ), _ = self.chain(self.mcu.gpio.request('led'), imp.Block(IndicatorLed(Led.Yellow))) - - (self.servo_led, ), _ = self.chain(self.mcu_servo.gpio.request('led'), imp.Block(IndicatorLed(Led.Yellow))) - (self.test_led, ), _ = self.chain(self.mcu_test.gpio.request_vector('led'), imp.Block(IndicatorLedArray(4, Led.Yellow))) - - # OLED MULTI DOMAIN - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.oled = imp.Block(Er_Oled_096_1c()) - self.connect(self.oled.vcc, self.v14) - self.connect(self.oled.pwr, self.v3v3) - self.connect(self.i2c, self.oled.i2c) - self.connect(self.mcu.gpio.request('oled_reset'), self.oled.reset) - - # CAMERA MULTI DOMAIN - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.cam = imp.Block(Ov2640_Fpc24()) - self.connect(self.cam.pwr, self.v3v3) - self.connect(self.cam.pwr_analog, self.v2v5) - self.connect(self.cam.pwr_digital, self.v1v2) - self.connect(self.mcu.with_mixin(IoControllerDvp8()).dvp8.request('cam'), self.cam.dvp8) - self.connect(self.cam.sio, self.i2c) - - # VBATT DOMAIN - with self.implicit_connect( - ImplicitConnect(self.vbatt, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - for (i, servo) in self.servos.items(): - self.connect(self.vbatt, servo.pwr) - self.connect(self.gnd, servo.gnd) - if int(i) < 4: # 0-3 connected to ESP directly - self.connect(self.mcu.gpio.request(f'servo{i}'), servo.pwm) - self.connect(self.mcu.adc.request(f'servo{i}_fb'), servo.fb) - else: # rest connected to STM as IO expander - self.connect(self.mcu_servo.gpio.request(f'servo{i}'), servo.pwm) - self.connect(self.mcu_servo.adc.request(f'servo{i}_fb'), servo.fb) - - for (i, servo) in self.servos_cam.items(): - self.connect(self.vbatt, servo.pwr) - self.connect(self.gnd, servo.gnd) - self.connect(self.mcu_servo.gpio.request(f'servo_cam{i}'), servo.pwm) - self.connect(self.mcu_servo.adc.request(f'servo_cam{i}_fb'), servo.fb) - - (self.rgbs, ), _ = self.chain( - self.mcu.gpio.request('rgb'), - imp.Block(NeopixelArray(10))) - - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['mcu_servo'], Stm32f103_48), - (['mcu_test'], Rp2040), - (['reg_3v3'], Ap7215), - (['reg_2v5'], Xc6206p), - (['reg_1v2'], Xc6206p), - (['reg_14v'], Tps61040), - (['batt', 'conn'], JstPhKVertical), - (['mcu_servo', 'swd', 'conn'], TagConnectNonLegged), - (['mcu_test', 'swd', 'conn'], TagConnectNonLegged), - ], - instance_values=[ - (['refdes_prefix'], 'R'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'servo0=34', - 'servo0_fb=38', - 'servo1=35', - 'servo1_fb=39', - 'servo2=4', - 'servo2_fb=5', - 'servo3=6', - 'servo3_fb=7', - - 'i2c.scl=10', - 'i2c.sda=9', - - 'rgb=32', - - 'led=33', - - 'cam.y2=25', - 'cam.y1=24', - 'cam.y3=23', - 'cam.y0=22', - 'cam.y4=21', - 'cam.pclk=20', - 'cam.y5=19', - 'cam.y6=18', - 'cam.xclk=17', - 'cam.y7=15', - 'cam.href=14', - 'cam.vsync=13', - - 'oled_reset=8', - ]), - (['mcu_servo', 'pin_assigns'], [ - 'servo4=41', - 'servo4_fb=10', - 'servo5=43', - 'servo5_fb=11', - 'servo6=45', - 'servo6_fb=12', - 'servo_cam0=46', - 'servo_cam0_fb=13', - 'servo7=26', - 'servo7_fb=14', - - 'servo8=32', - 'servo8_fb=19', - 'servo9=31', - 'servo9_fb=18', - 'servo10=30', - 'servo10_fb=17', - 'servo_cam1=29', - 'servo_cam1_fb=16', - 'servo11=28', - 'servo11_fb=15', - - 'led=33', - - 'i2c.scl=21', - 'i2c.sda=22', - ]), - (['mcu_servo', 'swd_swo_pin'], 'PB6'), # USART1_TX - (['mcu_test', 'pin_assigns'], [ - 'led_0=4', - 'led_1=12', - 'led_2=14', - 'led_3=16', - - 'i2c.scl=37', - 'i2c.sda=36', - ]), - (['mcu_test', 'swd_swo_pin'], 'GPIO16'), # UART0 TX - (['mcu', 'programming'], 'uart-auto'), - (['reg_14v', 'inductor', 'part'], "CBC3225T220KR"), - (['reg_14v', 'inductor', 'manual_frequency_rating'], Range(0, 17e6)), # 17MHz self-resonant - (['reg_14v', 'out_cap', 'cap', 'voltage_rating_derating'], 0.85), - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (SwdCortexTargetHeader, SwdCortexTargetTagConnect), - (Neopixel, Sk6812_Side_A), - (TestPoint, CompactKeystone5015), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - # the camera recommended specs are excessively tight, so loosen them a bit - (Ov2640_Fpc24, ['device', 'avdd', 'voltage_limits'], Range(2.3, 3.0)), # allow 2v5 - (Er_Oled_096_1c, ['device', 'vcc', 'voltage_limits'], Range(8, 19)), # abs maximum ratings - (ServoFeedbackConnector, ['pwr', 'current_draw'], Range(0.005, 0.005)), # ignore non-static draw - (Nonstrict3v3Compatible, ['nonstrict_3v3_compatible'], True), - ] - ) + """Implementation of the crawler robot, that implements what is needed to connect the interface blocks + as well as optional additional blocks. + """ + + @override + def contents(self) -> None: + super().contents() + + self.vbatt = self.connect(self.batt.pwr) + self.gnd = self.connect(self.batt.gnd) + + self.tp_vbatt = self.Block(VoltageTestPoint()).connected(self.batt.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.batt.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3), _ = self.chain( + self.vbatt, + imp.Block(VoltageRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + (self.reg_14v, self.tp_14v), _ = self.chain( + self.vbatt, imp.Block(VoltageRegulator(output_voltage=(13, 16) * Volt)), self.Block(VoltageTestPoint()) + ) + self.v14 = self.connect(self.reg_14v.pwr_out) + + (self.reg_2v5,), _ = self.chain( + self.vbatt, imp.Block(VoltageRegulator(output_voltage=2.5 * Volt(tol=0.05))) + ) + self.v2v5 = self.connect(self.reg_2v5.pwr_out) + + (self.reg_1v2,), _ = self.chain( + self.vbatt, imp.Block(VoltageRegulator(output_voltage=1.2 * Volt(tol=0.05))) + ) + self.v1v2 = self.connect(self.reg_1v2.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu_servo = imp.Block(IoController()) + self.connect(self.mcu.gpio.request("srv_rst"), self.mcu_servo.with_mixin(Resettable()).reset) + self.mcu_test = imp.Block(IoController()) # test revised subcircuit only + + self.i2c = self.mcu.i2c.request("i2c") + (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( + self.i2c, imp.Block(I2cPullup()), imp.Block(I2cTestPoint()) + ) + self.connect( + self.i2c, + self.imu.i2c, + self.mcu_servo.with_mixin(IoControllerI2cTarget()).i2c_target.request("i2c"), + self.mcu_test.with_mixin(IoControllerI2cTarget()).i2c_target.request("i2c"), + ) + + self.connect(self.v3v3, self.imu.pwr) + self.connect(self.gnd, self.imu.gnd) + + (self.led,), _ = self.chain(self.mcu.gpio.request("led"), imp.Block(IndicatorLed(Led.Yellow))) + + (self.servo_led,), _ = self.chain(self.mcu_servo.gpio.request("led"), imp.Block(IndicatorLed(Led.Yellow))) + (self.test_led,), _ = self.chain( + self.mcu_test.gpio.request_vector("led"), imp.Block(IndicatorLedArray(4, Led.Yellow)) + ) + + # OLED MULTI DOMAIN + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.oled = imp.Block(Er_Oled_096_1c()) + self.connect(self.oled.vcc, self.v14) + self.connect(self.oled.pwr, self.v3v3) + self.connect(self.i2c, self.oled.i2c) + self.connect(self.mcu.gpio.request("oled_reset"), self.oled.reset) + + # CAMERA MULTI DOMAIN + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.cam = imp.Block(Ov2640_Fpc24()) + self.connect(self.cam.pwr, self.v3v3) + self.connect(self.cam.pwr_analog, self.v2v5) + self.connect(self.cam.pwr_digital, self.v1v2) + self.connect(self.mcu.with_mixin(IoControllerDvp8()).dvp8.request("cam"), self.cam.dvp8) + self.connect(self.cam.sio, self.i2c) + + # VBATT DOMAIN + with self.implicit_connect( + ImplicitConnect(self.vbatt, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + for i, servo in self.servos.items(): + self.connect(self.vbatt, servo.pwr) + self.connect(self.gnd, servo.gnd) + if int(i) < 4: # 0-3 connected to ESP directly + self.connect(self.mcu.gpio.request(f"servo{i}"), servo.pwm) + self.connect(self.mcu.adc.request(f"servo{i}_fb"), servo.fb) + else: # rest connected to STM as IO expander + self.connect(self.mcu_servo.gpio.request(f"servo{i}"), servo.pwm) + self.connect(self.mcu_servo.adc.request(f"servo{i}_fb"), servo.fb) + + for i, servo in self.servos_cam.items(): + self.connect(self.vbatt, servo.pwr) + self.connect(self.gnd, servo.gnd) + self.connect(self.mcu_servo.gpio.request(f"servo_cam{i}"), servo.pwm) + self.connect(self.mcu_servo.adc.request(f"servo_cam{i}_fb"), servo.fb) + + (self.rgbs,), _ = self.chain(self.mcu.gpio.request("rgb"), imp.Block(NeopixelArray(10))) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["mcu_servo"], Stm32f103_48), + (["mcu_test"], Rp2040), + (["reg_3v3"], Ap7215), + (["reg_2v5"], Xc6206p), + (["reg_1v2"], Xc6206p), + (["reg_14v"], Tps61040), + (["batt", "conn"], JstPhKVertical), + (["mcu_servo", "swd", "conn"], TagConnectNonLegged), + (["mcu_test", "swd", "conn"], TagConnectNonLegged), + ], + instance_values=[ + (["refdes_prefix"], "R"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "servo0=34", + "servo0_fb=38", + "servo1=35", + "servo1_fb=39", + "servo2=4", + "servo2_fb=5", + "servo3=6", + "servo3_fb=7", + "i2c.scl=10", + "i2c.sda=9", + "rgb=32", + "led=33", + "cam.y2=25", + "cam.y1=24", + "cam.y3=23", + "cam.y0=22", + "cam.y4=21", + "cam.pclk=20", + "cam.y5=19", + "cam.y6=18", + "cam.xclk=17", + "cam.y7=15", + "cam.href=14", + "cam.vsync=13", + "oled_reset=8", + ], + ), + ( + ["mcu_servo", "pin_assigns"], + [ + "servo4=41", + "servo4_fb=10", + "servo5=43", + "servo5_fb=11", + "servo6=45", + "servo6_fb=12", + "servo_cam0=46", + "servo_cam0_fb=13", + "servo7=26", + "servo7_fb=14", + "servo8=32", + "servo8_fb=19", + "servo9=31", + "servo9_fb=18", + "servo10=30", + "servo10_fb=17", + "servo_cam1=29", + "servo_cam1_fb=16", + "servo11=28", + "servo11_fb=15", + "led=33", + "i2c.scl=21", + "i2c.sda=22", + ], + ), + (["mcu_servo", "swd_swo_pin"], "PB6"), # USART1_TX + ( + ["mcu_test", "pin_assigns"], + [ + "led_0=4", + "led_1=12", + "led_2=14", + "led_3=16", + "i2c.scl=37", + "i2c.sda=36", + ], + ), + (["mcu_test", "swd_swo_pin"], "GPIO16"), # UART0 TX + (["mcu", "programming"], "uart-auto"), + (["reg_14v", "inductor", "part"], "CBC3225T220KR"), + (["reg_14v", "inductor", "manual_frequency_rating"], Range(0, 17e6)), # 17MHz self-resonant + (["reg_14v", "out_cap", "cap", "voltage_rating_derating"], 0.85), + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (SwdCortexTargetHeader, SwdCortexTargetTagConnect), + (Neopixel, Sk6812_Side_A), + (TestPoint, CompactKeystone5015), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + # the camera recommended specs are excessively tight, so loosen them a bit + (Ov2640_Fpc24, ["device", "avdd", "voltage_limits"], Range(2.3, 3.0)), # allow 2v5 + (Er_Oled_096_1c, ["device", "vcc", "voltage_limits"], Range(8, 19)), # abs maximum ratings + (ServoFeedbackConnector, ["pwr", "current_draw"], Range(0.005, 0.005)), # ignore non-static draw + (Nonstrict3v3Compatible, ["nonstrict_3v3_compatible"], True), + ], + ) class RobotCrawlerTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(RobotCrawler) + def test_design(self) -> None: + compile_board_inplace(RobotCrawler) diff --git a/examples/test_robotdriver.py b/examples/test_robotdriver.py index f0c31fce2..b99add413 100644 --- a/examples/test_robotdriver.py +++ b/examples/test_robotdriver.py @@ -6,247 +6,243 @@ class MotorConnector(Connector, Block): - def __init__(self, current_draw: RangeLike): - super().__init__() - self.conn = self.Block(PassiveConnector()) + def __init__(self, current_draw: RangeLike): + super().__init__() + self.conn = self.Block(PassiveConnector()) - self.a = self.Export(self.conn.pins.request('2').adapt_to(DigitalSink( - current_draw=current_draw - ))) - self.b = self.Export(self.conn.pins.request('1').adapt_to(DigitalSink( - current_draw=current_draw - ))) + self.a = self.Export(self.conn.pins.request("2").adapt_to(DigitalSink(current_draw=current_draw))) + self.b = self.Export(self.conn.pins.request("1").adapt_to(DigitalSink(current_draw=current_draw))) class PwmConnector(Connector, Block): - def __init__(self, current_draw: RangeLike): - super().__init__() - self.conn = self.Block(PinHeader254()) + def __init__(self, current_draw: RangeLike): + super().__init__() + self.conn = self.Block(PinHeader254()) - self.pwm = self.Export(self.conn.pins.request('1').adapt_to(DigitalSink()), - [Input]) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSink( - current_draw=current_draw - )), [Power]) - self.gnd = self.Export(self.conn.pins.request('3').adapt_to(Ground()), - [Common]) + self.pwm = self.Export(self.conn.pins.request("1").adapt_to(DigitalSink()), [Input]) + self.pwr = self.Export(self.conn.pins.request("2").adapt_to(VoltageSink(current_draw=current_draw)), [Power]) + self.gnd = self.Export(self.conn.pins.request("3").adapt_to(Ground()), [Common]) class LedConnector(Connector, Block): - """Connector for external WS2812s.""" - def __init__(self, num_leds: FloatLike = 0): - super().__init__() - self.conn = self.Block(PassiveConnector()) - led_current = 36.6 + """Connector for external WS2812s.""" - self.vdd = self.Export(self.conn.pins.request('1').adapt_to(VoltageSink( - current_draw=(0*mAmp, led_current*num_leds))), - [Power]) - self.din = self.Export(self.conn.pins.request('2').adapt_to(DigitalSink()), [Input]) - self.gnd = self.Export(self.conn.pins.request('3').adapt_to(Ground()), [Common]) + def __init__(self, num_leds: FloatLike = 0): + super().__init__() + self.conn = self.Block(PassiveConnector()) + led_current = 36.6 + + self.vdd = self.Export( + self.conn.pins.request("1").adapt_to(VoltageSink(current_draw=(0 * mAmp, led_current * num_leds))), [Power] + ) + self.din = self.Export(self.conn.pins.request("2").adapt_to(DigitalSink()), [Input]) + self.gnd = self.Export(self.conn.pins.request("3").adapt_to(Ground()), [Common]) class RobotDriver(JlcBoardTop): - """Robot driver that uses a ESP32 (non-C) chip and includes a few more blocks - to use the extra available IOs - """ - @override - def contents(self) -> None: - super().contents() - - self.batt = self.Block(LipoConnector(actual_voltage=(3.7, 4.2)*Volt)) - - # actually on the 3V3 domain but need the battery output here - self.isense = self.Block(OpampCurrentSensor( - resistance=0.05*Ohm(tol=0.01), - ratio=Range.from_tolerance(20, 0.05), input_impedance=10*kOhm(tol=0.05) - )) - self.connect(self.isense.pwr_in, self.batt.pwr) - self.vbatt = self.connect(self.isense.pwr_out) - - self.gnd = self.connect(self.batt.gnd) - - self.tp_vbatt = self.Block(VoltageTestPoint()).connected(self.isense.pwr_out) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.batt.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.vbatt, - imp.Block(BuckConverter(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.i2c = self.mcu.i2c.request('i2c') - - self.tof = imp.Block(Vl53l0xArray(3)) - (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( - self.i2c, - imp.Block(I2cPullup()), imp.Block(I2cTestPoint()), - self.tof.i2c) - - self.lcd = imp.Block(Er_Oled_091_3()) - self.connect(self.mcu.spi.request('spi'), self.lcd.spi) - self.connect(self.lcd.cs, self.mcu.gpio.request('lcd_cs')) - self.connect(self.lcd.reset, self.mcu.gpio.request('lcd_reset')) - self.connect(self.lcd.dc, self.mcu.gpio.request('lcd_dc')) - - # IMU - self.imu = imp.Block(Lsm6ds3trc()) - self.connect(self.i2c, self.imu.i2c) - - # Current sensor - self.connect(self.isense.pwr, self.v3v3) - self.connect(self.isense.gnd, self.gnd) - self.connect(self.isense.ref, self.batt.gnd.as_analog_source()) - self.connect(self.isense.out, self.mcu.adc.request('isense')) - - self.expander = imp.Block(Pcf8574(0)) - self.connect(self.i2c, self.expander.i2c) - self.connect(self.expander.io.request_vector('tof_reset'), self.tof.reset) - - self.leds = imp.Block(IndicatorSinkLedArray(4)) - self.connect(self.expander.io.request_vector('led'), self.leds.signals) - - # SPEAKER AND LOW POWER VBATT DOMAIN - with self.implicit_connect( + """Robot driver that uses a ESP32 (non-C) chip and includes a few more blocks + to use the extra available IOs + """ + + @override + def contents(self) -> None: + super().contents() + + self.batt = self.Block(LipoConnector(actual_voltage=(3.7, 4.2) * Volt)) + + # actually on the 3V3 domain but need the battery output here + self.isense = self.Block( + OpampCurrentSensor( + resistance=0.05 * Ohm(tol=0.01), + ratio=Range.from_tolerance(20, 0.05), + input_impedance=10 * kOhm(tol=0.05), + ) + ) + self.connect(self.isense.pwr_in, self.batt.pwr) + self.vbatt = self.connect(self.isense.pwr_out) + + self.gnd = self.connect(self.batt.gnd) + + self.tp_vbatt = self.Block(VoltageTestPoint()).connected(self.isense.pwr_out) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.batt.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.vbatt, + imp.Block(BuckConverter(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.i2c = self.mcu.i2c.request("i2c") + + self.tof = imp.Block(Vl53l0xArray(3)) + (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( + self.i2c, imp.Block(I2cPullup()), imp.Block(I2cTestPoint()), self.tof.i2c + ) + + self.lcd = imp.Block(Er_Oled_091_3()) + self.connect(self.mcu.spi.request("spi"), self.lcd.spi) + self.connect(self.lcd.cs, self.mcu.gpio.request("lcd_cs")) + self.connect(self.lcd.reset, self.mcu.gpio.request("lcd_reset")) + self.connect(self.lcd.dc, self.mcu.gpio.request("lcd_dc")) + + # IMU + self.imu = imp.Block(Lsm6ds3trc()) + self.connect(self.i2c, self.imu.i2c) + + # Current sensor + self.connect(self.isense.pwr, self.v3v3) + self.connect(self.isense.gnd, self.gnd) + self.connect(self.isense.ref, self.batt.gnd.as_analog_source()) + self.connect(self.isense.out, self.mcu.adc.request("isense")) + + self.expander = imp.Block(Pcf8574(0)) + self.connect(self.i2c, self.expander.i2c) + self.connect(self.expander.io.request_vector("tof_reset"), self.tof.reset) + + self.leds = imp.Block(IndicatorSinkLedArray(4)) + self.connect(self.expander.io.request_vector("led"), self.leds.signals) + + # SPEAKER AND LOW POWER VBATT DOMAIN + with self.implicit_connect( ImplicitConnect(self.vbatt, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.spk_tp, self.spk_drv, self.spk), self.spk_chain = self.chain( - self.mcu.with_mixin(IoControllerDac()).dac.request('spk'), - self.Block(AnalogTestPoint()), - imp.Block(Tpa2005d1(gain=Range.from_tolerance(10, 0.2))), - self.Block(Speaker())) - - self.ws2812bArray = imp.Block(NeopixelArray(5)) - self.connect(self.mcu.gpio.request('ledArray'), self.ws2812bArray.din) - - self.led_pixel = imp.Block(LedConnector()) - self.connect(self.ws2812bArray.dout, self.led_pixel.din) - - # MOTORS AND SERVOS - with self.implicit_connect( + ) as imp: + (self.spk_tp, self.spk_drv, self.spk), self.spk_chain = self.chain( + self.mcu.with_mixin(IoControllerDac()).dac.request("spk"), + self.Block(AnalogTestPoint()), + imp.Block(Tpa2005d1(gain=Range.from_tolerance(10, 0.2))), + self.Block(Speaker()), + ) + + self.ws2812bArray = imp.Block(NeopixelArray(5)) + self.connect(self.mcu.gpio.request("ledArray"), self.ws2812bArray.din) + + self.led_pixel = imp.Block(LedConnector()) + self.connect(self.ws2812bArray.dout, self.led_pixel.din) + + # MOTORS AND SERVOS + with self.implicit_connect( ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.motor_driver1 = imp.Block(Drv8833()) - self.connect(self.vbatt, self.motor_driver1.pwr) - self.connect(self.mcu.gpio.request('motor_1a1'), self.motor_driver1.ain1) - self.connect(self.mcu.gpio.request('motor_1a2'), self.motor_driver1.ain2) - self.connect(self.mcu.gpio.request('motor_1b1'), self.motor_driver1.bin1) - self.connect(self.mcu.gpio.request('motor_1b2'), self.motor_driver1.bin2) - - self.m1_a = self.Block(MotorConnector((-600, 600)*mAmp)) - self.connect(self.m1_a.a, self.motor_driver1.aout1) - self.connect(self.m1_a.b, self.motor_driver1.aout2) - self.m1_b = self.Block(MotorConnector((-600, 600)*mAmp)) - self.connect(self.m1_b.a, self.motor_driver1.bout1) - self.connect(self.m1_b.b, self.motor_driver1.bout2) - - self.motor_driver2 = imp.Block(Drv8833()) - self.connect(self.vbatt, self.motor_driver2.pwr) - self.connect(self.mcu.gpio.request('motor_2a1'), self.motor_driver2.ain1) - self.connect(self.mcu.gpio.request('motor_2a2'), self.motor_driver2.ain2) - self.connect(self.mcu.gpio.request('motor_2b1'), self.motor_driver2.bin1) - self.connect(self.mcu.gpio.request('motor_2b2'), self.motor_driver2.bin2) - self.connect(self.motor_driver1.sleep, self.motor_driver2.sleep, self.isense.pwr_out.as_digital_source()) - - self.m2_a = self.Block(MotorConnector((-600, 600)*mAmp)) - self.connect(self.m2_a.a, self.motor_driver2.aout1) - self.connect(self.m2_a.b, self.motor_driver2.aout2) - self.m2_b = self.Block(MotorConnector((-600, 600)*mAmp)) - self.connect(self.m2_b.a, self.motor_driver2.bout1) - self.connect(self.m2_b.b, self.motor_driver2.bout2) - - self.servo = self.Block(PwmConnector((0, 0)*mAmp)) # TODO current modeling - self.connect(self.vbatt, self.servo.pwr) - self.connect(self.gnd, self.servo.gnd) - self.connect(self.mcu.gpio.request('pwm'), self.servo.pwm) - - @override - def multipack(self) -> None: - self.led_res = self.PackedBlock(ResistorArray()) - self.pack(self.led_res.elements.request('0'), ['leds', 'led[0]', 'res']) - self.pack(self.led_res.elements.request('1'), ['leds', 'led[1]', 'res']) - self.pack(self.led_res.elements.request('2'), ['leds', 'led[2]', 'res']) - self.pack(self.led_res.elements.request('3'), ['leds', 'led[3]', 'res']) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32_Wroom_32), - (['reg_3v3'], Ap3418), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'spi.miso=NC', - 'i2c.scl=16', - 'i2c.sda=14', - - 'spk=11', # only 10 and 11 are DAC out - - 'lcd_cs=13', - 'lcd_reset=12', - 'lcd_dc=10', - 'spi.sck=9', - 'spi.mosi=8', - - 'ledArray=23', - - 'isense=4', # use an input only pin - - 'motor_2a1=26', - 'motor_2a2=27', - 'motor_2b2=28', - 'motor_2b1=29', - 'motor_1a1=30', - 'motor_1a2=31', - 'motor_1b2=33', - 'motor_1b1=36', - - 'pwm=37', - ]), - (['expander', 'pin_assigns'], [ - 'led_0=4', - 'led_1=5', - 'led_2=6', - 'led_3=7', - 'tof_reset_0=10', - 'tof_reset_1=11', - 'tof_reset_2=12', - ]), - (['isense', 'out', 'signal_out'], Range(0.1, 2.45)), # trade range for resolution - (['isense', 'sense', 'res', 'res', 'footprint_spec'], 'Resistor_SMD:R_2512_6332Metric'), - (['isense', 'sense', 'res', 'res', 'require_basic_part'], False), - - # JLC does not have frequency specs, must be checked TODO - (['reg_3v3', 'power_path', 'inductor', 'frequency'], Range(0, 0)), - (['reg_3v3', 'power_path', 'efficiency'], Range(1.0, 1.0)), # waive this check - ], - class_refinements=[ - (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified - (Vl53l0x, Vl53l0xConnector), - (TestPoint, TeRc), - (Speaker, ConnectorSpeaker), - (Neopixel, Ws2812b), - ], - class_values=[ - (Nonstrict3v3Compatible, ['nonstrict_3v3_compatible'], True), - ], - ) + ) as imp: + self.motor_driver1 = imp.Block(Drv8833()) + self.connect(self.vbatt, self.motor_driver1.pwr) + self.connect(self.mcu.gpio.request("motor_1a1"), self.motor_driver1.ain1) + self.connect(self.mcu.gpio.request("motor_1a2"), self.motor_driver1.ain2) + self.connect(self.mcu.gpio.request("motor_1b1"), self.motor_driver1.bin1) + self.connect(self.mcu.gpio.request("motor_1b2"), self.motor_driver1.bin2) + + self.m1_a = self.Block(MotorConnector((-600, 600) * mAmp)) + self.connect(self.m1_a.a, self.motor_driver1.aout1) + self.connect(self.m1_a.b, self.motor_driver1.aout2) + self.m1_b = self.Block(MotorConnector((-600, 600) * mAmp)) + self.connect(self.m1_b.a, self.motor_driver1.bout1) + self.connect(self.m1_b.b, self.motor_driver1.bout2) + + self.motor_driver2 = imp.Block(Drv8833()) + self.connect(self.vbatt, self.motor_driver2.pwr) + self.connect(self.mcu.gpio.request("motor_2a1"), self.motor_driver2.ain1) + self.connect(self.mcu.gpio.request("motor_2a2"), self.motor_driver2.ain2) + self.connect(self.mcu.gpio.request("motor_2b1"), self.motor_driver2.bin1) + self.connect(self.mcu.gpio.request("motor_2b2"), self.motor_driver2.bin2) + self.connect(self.motor_driver1.sleep, self.motor_driver2.sleep, self.isense.pwr_out.as_digital_source()) + + self.m2_a = self.Block(MotorConnector((-600, 600) * mAmp)) + self.connect(self.m2_a.a, self.motor_driver2.aout1) + self.connect(self.m2_a.b, self.motor_driver2.aout2) + self.m2_b = self.Block(MotorConnector((-600, 600) * mAmp)) + self.connect(self.m2_b.a, self.motor_driver2.bout1) + self.connect(self.m2_b.b, self.motor_driver2.bout2) + + self.servo = self.Block(PwmConnector((0, 0) * mAmp)) # TODO current modeling + self.connect(self.vbatt, self.servo.pwr) + self.connect(self.gnd, self.servo.gnd) + self.connect(self.mcu.gpio.request("pwm"), self.servo.pwm) + + @override + def multipack(self) -> None: + self.led_res = self.PackedBlock(ResistorArray()) + self.pack(self.led_res.elements.request("0"), ["leds", "led[0]", "res"]) + self.pack(self.led_res.elements.request("1"), ["leds", "led[1]", "res"]) + self.pack(self.led_res.elements.request("2"), ["leds", "led[2]", "res"]) + self.pack(self.led_res.elements.request("3"), ["leds", "led[3]", "res"]) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32_Wroom_32), + (["reg_3v3"], Ap3418), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "spi.miso=NC", + "i2c.scl=16", + "i2c.sda=14", + "spk=11", # only 10 and 11 are DAC out + "lcd_cs=13", + "lcd_reset=12", + "lcd_dc=10", + "spi.sck=9", + "spi.mosi=8", + "ledArray=23", + "isense=4", # use an input only pin + "motor_2a1=26", + "motor_2a2=27", + "motor_2b2=28", + "motor_2b1=29", + "motor_1a1=30", + "motor_1a2=31", + "motor_1b2=33", + "motor_1b1=36", + "pwm=37", + ], + ), + ( + ["expander", "pin_assigns"], + [ + "led_0=4", + "led_1=5", + "led_2=6", + "led_3=7", + "tof_reset_0=10", + "tof_reset_1=11", + "tof_reset_2=12", + ], + ), + (["isense", "out", "signal_out"], Range(0.1, 2.45)), # trade range for resolution + (["isense", "sense", "res", "res", "footprint_spec"], "Resistor_SMD:R_2512_6332Metric"), + (["isense", "sense", "res", "res", "require_basic_part"], False), + # JLC does not have frequency specs, must be checked TODO + (["reg_3v3", "power_path", "inductor", "frequency"], Range(0, 0)), + (["reg_3v3", "power_path", "efficiency"], Range(1.0, 1.0)), # waive this check + ], + class_refinements=[ + (PassiveConnector, JstPhKVertical), # default connector series unless otherwise specified + (Vl53l0x, Vl53l0xConnector), + (TestPoint, TeRc), + (Speaker, ConnectorSpeaker), + (Neopixel, Ws2812b), + ], + class_values=[ + (Nonstrict3v3Compatible, ["nonstrict_3v3_compatible"], True), + ], + ) class RobotDriverTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(RobotDriver) + def test_design(self) -> None: + compile_board_inplace(RobotDriver) diff --git a/examples/test_robotowl.py b/examples/test_robotowl.py index 4fe597cbd..179dc2f99 100644 --- a/examples/test_robotowl.py +++ b/examples/test_robotowl.py @@ -8,156 +8,157 @@ class PhotodiodeSensor(LightSensor, KiCadSchematicBlock, Block): - """Simple photodiode-based light sensor""" - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground.empty(), [Common]) - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.out = self.Port(AnalogSource.empty(), [Output]) - - @override - def contents(self) -> None: - super().contents() - self.import_kicad( - self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'pwr': VoltageSink(), - 'gnd': Ground(), - 'out': AnalogSource( - voltage_out=self.pwr.link().voltage.hull(self.gnd.link().voltage), - signal_out=self.pwr.link().voltage.hull(self.gnd.link().voltage), - # TODO: what is the impedance? - ), - }) + """Simple photodiode-based light sensor""" + + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.out = self.Port(AnalogSource.empty(), [Output]) + + @override + def contents(self) -> None: + super().contents() + self.import_kicad( + self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "pwr": VoltageSink(), + "gnd": Ground(), + "out": AnalogSource( + voltage_out=self.pwr.link().voltage.hull(self.gnd.link().voltage), + signal_out=self.pwr.link().voltage.hull(self.gnd.link().voltage), + # TODO: what is the impedance? + ), + }, + ) class RobotOwl(JlcBoardTop): - """Controller for a robot owl with a ESP32S3 dev board w/ camera, audio, and peripherals. - - Note, 9 free IOs available - 3 I2S out - 2 I2S in (digital PDM) - 2 PWM - 2 I2C - optionally multiplexed onto camera pins - 1 NPX - 1 analog - """ - @override - def contents(self) -> None: - super().contents() - - self.mcu = self.Block(IoController()) - mcu_pwr = self.mcu.with_mixin(IoControllerPowerOut()) - mcu_usb = self.mcu.with_mixin(IoControllerUsbOut()) - mcu_i2s = self.mcu.with_mixin(IoControllerI2s()) - - self.gnd = self.connect(self.mcu.gnd) - self.vusb = self.connect(mcu_usb.vusb_out) - self.v3v3 = self.connect(mcu_pwr.pwr_out) - - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.mcu.gnd) - self.tp_usb = self.Block(VoltageTestPoint()).connected(mcu_usb.vusb_out) - self.tp_3v3 = self.Block(VoltageTestPoint()).connected(mcu_pwr.pwr_out) - - (self.reg_12v, self.tp_12v), _ = self.chain( - self.vusb, - self.Block(BoostConverter(output_voltage=(12, 15)*Volt)), - self.Block(VoltageTestPoint()) - ) - self.connect(self.reg_12v.gnd, self.gnd) - self.v12 = self.connect(self.reg_12v.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mic = imp.Block(Sd18ob261()) - self.connect(self.mic.clk, self.mcu.gpio.request('mic_clk')) - self.connect(self.mic.data, self.mcu.gpio.request('mic_data')) - - (self.photodiode, ), _ = self.chain( - imp.Block(PhotodiodeSensor()), - self.mcu.adc.request('photodiode') - ) - - self.oled22 = imp.Block(Er_Oled022_1()) - self.connect(self.v3v3, self.oled22.pwr) - self.connect(self.v12, self.oled22.vcc) - self.connect(self.oled22.i2c, self.mcu.i2c.request('oled')) - (self.oled_rst, self.oled_pull), _ = self.chain( - imp.Block(Apx803s(reset_threshold=(2.88, 2.98)*Volt)), # -29 variant used on Adafruit boards - imp.Block(PullupResistor(10*kOhm(tol=0.05))), - self.oled22.reset - ) - - # VBATT DOMAIN - with self.implicit_connect( - ImplicitConnect(self.vusb, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.spk_drv, self.spk), _ = self.chain( - mcu_i2s.i2s.request('speaker'), - imp.Block(Max98357a()), - self.Block(Speaker()) - ) - - self.servo = ElementDict[PwmConnector]() - for i in range(2): - (self.servo[i], ), _ = self.chain( - self.mcu.gpio.request(f'servo{i}'), - imp.Block(PwmConnector((0, 200)*mAmp)) + """Controller for a robot owl with a ESP32S3 dev board w/ camera, audio, and peripherals. + + Note, 9 free IOs available + 3 I2S out + 2 I2S in (digital PDM) + 2 PWM + 2 I2C - optionally multiplexed onto camera pins + 1 NPX + 1 analog + """ + + @override + def contents(self) -> None: + super().contents() + + self.mcu = self.Block(IoController()) + mcu_pwr = self.mcu.with_mixin(IoControllerPowerOut()) + mcu_usb = self.mcu.with_mixin(IoControllerUsbOut()) + mcu_i2s = self.mcu.with_mixin(IoControllerI2s()) + + self.gnd = self.connect(self.mcu.gnd) + self.vusb = self.connect(mcu_usb.vusb_out) + self.v3v3 = self.connect(mcu_pwr.pwr_out) + + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.mcu.gnd) + self.tp_usb = self.Block(VoltageTestPoint()).connected(mcu_usb.vusb_out) + self.tp_3v3 = self.Block(VoltageTestPoint()).connected(mcu_pwr.pwr_out) + + (self.reg_12v, self.tp_12v), _ = self.chain( + self.vusb, self.Block(BoostConverter(output_voltage=(12, 15) * Volt)), self.Block(VoltageTestPoint()) + ) + self.connect(self.reg_12v.gnd, self.gnd) + self.v12 = self.connect(self.reg_12v.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mic = imp.Block(Sd18ob261()) + self.connect(self.mic.clk, self.mcu.gpio.request("mic_clk")) + self.connect(self.mic.data, self.mcu.gpio.request("mic_data")) + + (self.photodiode,), _ = self.chain(imp.Block(PhotodiodeSensor()), self.mcu.adc.request("photodiode")) + + self.oled22 = imp.Block(Er_Oled022_1()) + self.connect(self.v3v3, self.oled22.pwr) + self.connect(self.v12, self.oled22.vcc) + self.connect(self.oled22.i2c, self.mcu.i2c.request("oled")) + (self.oled_rst, self.oled_pull), _ = self.chain( + imp.Block(Apx803s(reset_threshold=(2.88, 2.98) * Volt)), # -29 variant used on Adafruit boards + imp.Block(PullupResistor(10 * kOhm(tol=0.05))), + self.oled22.reset, + ) + + # VBATT DOMAIN + with self.implicit_connect( + ImplicitConnect(self.vusb, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.spk_drv, self.spk), _ = self.chain( + mcu_i2s.i2s.request("speaker"), imp.Block(Max98357a()), self.Block(Speaker()) + ) + + self.servo = ElementDict[PwmConnector]() + for i in range(2): + (self.servo[i],), _ = self.chain( + self.mcu.gpio.request(f"servo{i}"), imp.Block(PwmConnector((0, 200) * mAmp)) + ) + + (self.ws2812bArray, self.extNeopixels), _ = self.chain( + self.mcu.gpio.request("ws2812"), imp.Block(NeopixelArray(12)), imp.Block(LedConnector()) + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Freenove_Esp32s3_Wroom), + (["reg_12v"], Ap3012), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "photodiode=GPIO1", + "servo0=25", + "servo1=24", + "mic_data=19", + "mic_clk=12", + "speaker.sd=36", + "speaker.sck=35", + "speaker.ws=37", + "ws2812=26", # WS2812 + "oled=CAM_SCCB", + ], + ), + (["mcu", "fp_footprint"], "edg:Freenove_ESP32S3-WROOM_Expansion"), + (["mcu", "vusb_out", "current_limits"], Range(0, 3)), + ( + ["photodiode", "out", "signal_out"], + Range(0, 2.2), + ), # discard the extra range to make it ESP compatible + (["reg_12v", "power_path", "inductor", "part"], "CBC3225T470KR"), + (["reg_12v", "power_path", "inductor", "manual_frequency_rating"], Range(0, 7e6)), + # compatibility with what was manufactured + ( + ["spk_drv", "ic", "footprint_spec"], + "Package_BGA:Maxim_WLP-9_1.595x1.415_Layout3x3_P0.4mm_Ball0.27mm_Pad0.25mm_NSMD", + ), + ], + class_refinements=[ + (PassiveConnector, PinHeader254), # default connector series unless otherwise specified + (PinHeader254, PinHeader254Horizontal), + (TestPoint, CompactKeystone5015), + (Speaker, ConnectorSpeaker), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + (Er_Oled022_1, ["iref_res", "resistance"], Range.from_tolerance(820e3, 0.1)), # use a basic part + (Er_Oled022_1, ["device", "vcc", "voltage_limits"], Range(12, 15)), # allow it to be a bit lower + ], ) - - (self.ws2812bArray, self.extNeopixels), _ = self.chain( - self.mcu.gpio.request('ws2812'), - imp.Block(NeopixelArray(12)), - imp.Block(LedConnector()) - ) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Freenove_Esp32s3_Wroom), - (['reg_12v'], Ap3012), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'photodiode=GPIO1', - 'servo0=25', - 'servo1=24', - 'mic_data=19', - 'mic_clk=12', - 'speaker.sd=36', - 'speaker.sck=35', - 'speaker.ws=37', - 'ws2812=26', # WS2812 - 'oled=CAM_SCCB', - ]), - (['mcu', 'fp_footprint'], 'edg:Freenove_ESP32S3-WROOM_Expansion'), - (['mcu', 'vusb_out', 'current_limits'], Range(0, 3)), - (['photodiode', 'out', 'signal_out'], Range(0, 2.2)), # discard the extra range to make it ESP compatible - - (['reg_12v', 'power_path', 'inductor', 'part'], "CBC3225T470KR"), - (['reg_12v', 'power_path', 'inductor', 'manual_frequency_rating'], Range(0, 7e6)), - # compatibility with what was manufactured - (['spk_drv', 'ic', 'footprint_spec'], 'Package_BGA:Maxim_WLP-9_1.595x1.415_Layout3x3_P0.4mm_Ball0.27mm_Pad0.25mm_NSMD') - ], - class_refinements=[ - (PassiveConnector, PinHeader254), # default connector series unless otherwise specified - (PinHeader254, PinHeader254Horizontal), - (TestPoint, CompactKeystone5015), - (Speaker, ConnectorSpeaker), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - (Er_Oled022_1, ["iref_res", "resistance"], Range.from_tolerance(820e3, 0.1)), # use a basic part - (Er_Oled022_1, ["device", "vcc", "voltage_limits"], Range(12, 15)), # allow it to be a bit lower - ], - ) class RobotOwlTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(RobotOwl) + def test_design(self) -> None: + compile_board_inplace(RobotOwl) diff --git a/examples/test_seven_segment.py b/examples/test_seven_segment.py index 8bb67cc87..7924ac3de 100644 --- a/examples/test_seven_segment.py +++ b/examples/test_seven_segment.py @@ -6,134 +6,137 @@ class SevenSegment(JlcBoardTop): - """RGB 7-segment clock using Neopixels. - """ - @override - def contents(self) -> None: - super().contents() - - self.pwr_conn = self.Block(LipoConnector(voltage=(4.5, 5.5)*Volt, actual_voltage=(4.5, 5.5)*Volt)) - self.pwr = self.connect(self.pwr_conn.pwr) - self.gnd = self.connect(self.pwr_conn.gnd) - - self.tp_pwr = self.Block(VoltageTestPoint()).connected(self.pwr_conn.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.pwr_conn.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.pwr, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - self.mcu.with_mixin(IoControllerWifi()) - - (self.ledr, ), _ = self.chain(self.mcu.gpio.request('ledr'), imp.Block(IndicatorLed(Led.Red))) - (self.ledg, ), _ = self.chain(self.mcu.gpio.request('ledg'), imp.Block(IndicatorLed(Led.Green))) - (self.ledb, ), _ = self.chain(self.mcu.gpio.request('ledb'), imp.Block(IndicatorLed(Led.Blue))) - - self.sw = ElementDict[DigitalSwitch]() - for i in range(4): - (self.sw[i], ), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request(f'sw{i}')) - - self.i2c = self.mcu.i2c.request('i2c') - (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( - self.i2c, - imp.Block(I2cPullup()), imp.Block(I2cTestPoint())) - - self.env = imp.Block(Bme680()) - self.connect(self.i2c, self.env.i2c) - self.als = imp.Block(Bh1750()) - self.connect(self.i2c, self.als.i2c) - - # 5V DOMAIN - with self.implicit_connect( - ImplicitConnect(self.pwr, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.rgb_shift, self.rgb_tp), _ = self.chain( - self.mcu.gpio.request('rgb'), - imp.Block(L74Ahct1g125()), - imp.Block(DigitalTestPoint())) - - self.digit = ElementDict[NeopixelArray]() - for i in range(4): - self.digit[i] = imp.Block(NeopixelArray(2*7)) - self.center = imp.Block(NeopixelArray(2)) - self.meta = imp.Block(NeopixelArray(2)) - - self.connect(self.rgb_shift.output, self.digit[0].din) - self.connect(self.digit[0].dout, self.digit[1].din) - self.connect(self.digit[1].dout, self.meta.din) - self.connect(self.meta.dout, self.center.din) - self.connect(self.center.dout, self.digit[2].din) - self.connect(self.digit[2].dout, self.digit[3].din) - - (self.spk_dac, self.spk_tp, self.spk_drv, self.spk), self.spk_chain = self.chain( - self.mcu.gpio.request('spk'), - imp.Block(LowPassRcDac(1*kOhm(tol=0.05), 5*kHertz(tol=0.5))), - self.Block(AnalogTestPoint()), - imp.Block(Tpa2005d1(gain=Range.from_tolerance(10, 0.2))), - self.Block(Speaker())) - - self.v5v_sense = imp.Block(VoltageSenseDivider(full_scale_voltage=3.0*Volt(tol=0.1), impedance=(1, 10)*kOhm)) - self.connect(self.v5v_sense.input, self.pwr) - self.connect(self.v5v_sense.output, self.mcu.adc.request('v5v_sense')) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['reg_3v3'], Ldl1117), - (['pwr_conn', 'conn'], JstPhKVertical), - (['spk', 'conn'], JstPhKVertical), - ], - instance_values=[ - (['refdes_prefix'], 'C'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - 'ledr=4', - 'ledg=5', - 'ledb=6', - 'i2c.sda=8', - 'i2c.scl=9', - 'rgb=12', - - 'sw0=32', - 'sw1=33', - 'sw2=34', - 'sw3=35', - - 'v5v_sense=7', - 'spk=31', - ]), - (['mcu', 'programming'], 'uart-auto') - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (Neopixel, Sk6805_Ec15), - (Switch, Skrtlae010), - (Speaker, ConnectorSpeaker), - (TestPoint, CompactKeystone5015), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), # RH-5015, which is actually in stock - (Tpa2005d1, ['ic', 'lcsc_part'], 'C113367'), # use PAM8302AASCR, since TPA2005D1 is out of stock - ] - ) + """RGB 7-segment clock using Neopixels.""" + + @override + def contents(self) -> None: + super().contents() + + self.pwr_conn = self.Block(LipoConnector(voltage=(4.5, 5.5) * Volt, actual_voltage=(4.5, 5.5) * Volt)) + self.pwr = self.connect(self.pwr_conn.pwr) + self.gnd = self.connect(self.pwr_conn.gnd) + + self.tp_pwr = self.Block(VoltageTestPoint()).connected(self.pwr_conn.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.pwr_conn.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.pwr, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + self.mcu.with_mixin(IoControllerWifi()) + + (self.ledr,), _ = self.chain(self.mcu.gpio.request("ledr"), imp.Block(IndicatorLed(Led.Red))) + (self.ledg,), _ = self.chain(self.mcu.gpio.request("ledg"), imp.Block(IndicatorLed(Led.Green))) + (self.ledb,), _ = self.chain(self.mcu.gpio.request("ledb"), imp.Block(IndicatorLed(Led.Blue))) + + self.sw = ElementDict[DigitalSwitch]() + for i in range(4): + (self.sw[i],), _ = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request(f"sw{i}")) + + self.i2c = self.mcu.i2c.request("i2c") + (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( + self.i2c, imp.Block(I2cPullup()), imp.Block(I2cTestPoint()) + ) + + self.env = imp.Block(Bme680()) + self.connect(self.i2c, self.env.i2c) + self.als = imp.Block(Bh1750()) + self.connect(self.i2c, self.als.i2c) + + # 5V DOMAIN + with self.implicit_connect( + ImplicitConnect(self.pwr, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.rgb_shift, self.rgb_tp), _ = self.chain( + self.mcu.gpio.request("rgb"), imp.Block(L74Ahct1g125()), imp.Block(DigitalTestPoint()) + ) + + self.digit = ElementDict[NeopixelArray]() + for i in range(4): + self.digit[i] = imp.Block(NeopixelArray(2 * 7)) + self.center = imp.Block(NeopixelArray(2)) + self.meta = imp.Block(NeopixelArray(2)) + + self.connect(self.rgb_shift.output, self.digit[0].din) + self.connect(self.digit[0].dout, self.digit[1].din) + self.connect(self.digit[1].dout, self.meta.din) + self.connect(self.meta.dout, self.center.din) + self.connect(self.center.dout, self.digit[2].din) + self.connect(self.digit[2].dout, self.digit[3].din) + + (self.spk_dac, self.spk_tp, self.spk_drv, self.spk), self.spk_chain = self.chain( + self.mcu.gpio.request("spk"), + imp.Block(LowPassRcDac(1 * kOhm(tol=0.05), 5 * kHertz(tol=0.5))), + self.Block(AnalogTestPoint()), + imp.Block(Tpa2005d1(gain=Range.from_tolerance(10, 0.2))), + self.Block(Speaker()), + ) + + self.v5v_sense = imp.Block( + VoltageSenseDivider(full_scale_voltage=3.0 * Volt(tol=0.1), impedance=(1, 10) * kOhm) + ) + self.connect(self.v5v_sense.input, self.pwr) + self.connect(self.v5v_sense.output, self.mcu.adc.request("v5v_sense")) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["reg_3v3"], Ldl1117), + (["pwr_conn", "conn"], JstPhKVertical), + (["spk", "conn"], JstPhKVertical), + ], + instance_values=[ + (["refdes_prefix"], "C"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + "ledr=4", + "ledg=5", + "ledb=6", + "i2c.sda=8", + "i2c.scl=9", + "rgb=12", + "sw0=32", + "sw1=33", + "sw2=34", + "sw3=35", + "v5v_sense=7", + "spk=31", + ], + ), + (["mcu", "programming"], "uart-auto"), + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (Neopixel, Sk6805_Ec15), + (Switch, Skrtlae010), + (Speaker, ConnectorSpeaker), + (TestPoint, CompactKeystone5015), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), # RH-5015, which is actually in stock + (Tpa2005d1, ["ic", "lcsc_part"], "C113367"), # use PAM8302AASCR, since TPA2005D1 is out of stock + ], + ) class SevenSegmentTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(SevenSegment) + def test_design(self) -> None: + compile_board_inplace(SevenSegment) diff --git a/examples/test_simon.py b/examples/test_simon.py index 3dd56f025..93ed9a490 100644 --- a/examples/test_simon.py +++ b/examples/test_simon.py @@ -6,132 +6,138 @@ class DomeButtonConnector(Connector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.led_a = self.Port(DigitalSink( - voltage_limits=(0, 15) * Volt, # arbitrary +3v tolerance - current_draw=(0, 10)*mAmp # TODO characterize current draw - )) - self.led_k = self.Port(Ground(), [Common]) # TODO should be agnostic to high / low sided drive - self.sw2 = self.Port(Ground(), [Common]) - self.sw1 = self.Port(DigitalSource.low_from_supply(self.sw2)) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - 'J', 'Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical', - { - '1': self.led_a, - '2': self.led_k, - '3': self.sw1, - '4': self.sw2, - }, - mfr='Sparkfun', part='COM-09181' # TODO different colors - ) + def __init__(self) -> None: + super().__init__() + + self.led_a = self.Port( + DigitalSink( + voltage_limits=(0, 15) * Volt, # arbitrary +3v tolerance + current_draw=(0, 10) * mAmp, # TODO characterize current draw + ) + ) + self.led_k = self.Port(Ground(), [Common]) # TODO should be agnostic to high / low sided drive + self.sw2 = self.Port(Ground(), [Common]) + self.sw1 = self.Port(DigitalSource.low_from_supply(self.sw2)) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "J", + "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical", + { + "1": self.led_a, + "2": self.led_k, + "3": self.sw1, + "4": self.sw2, + }, + mfr="Sparkfun", + part="COM-09181", # TODO different colors + ) class Simon(BoardTop): - @override - def contents(self) -> None: - super().contents() - - self.mcu = self.Block(IoController()) - mcu_pwr = self.mcu.with_mixin(IoControllerPowerOut()) - mcu_usb = self.mcu.with_mixin(IoControllerUsbOut()) - - self.v5v = self.connect(mcu_usb.vusb_out) - self.v3v3 = self.connect(mcu_pwr.pwr_out) - self.gnd = self.connect(self.mcu.gnd) - - with self.implicit_connect( - ImplicitConnect(self.v5v, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.spk_drv, self.spk), _ = self.chain( - self.mcu.with_mixin(IoControllerDac()).dac.request('spk'), - imp.Block(Lm4871()), - self.Block(Speaker())) - - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.rgb = imp.Block(IndicatorSinkRgbLed()) # status RGB - self.connect(self.mcu.gpio.request_vector('rgb'), self.rgb.signals) - - (self.sw, self.sw_pull), _ = self.chain( - imp.Block(DigitalSwitch()), imp.Block(PullupResistor(10 * kOhm(tol=0.05))), - self.mcu.gpio.request('sw')) - - self.btn = ElementDict[DomeButtonConnector]() - self.btn_pull = ElementDict[PullupResistor]() - self.btn_drv = ElementDict[HighSideSwitch]() # TODO move to 12v - self.btn_zeroed_current = ElementDict[HighSideSwitch]() # TODO move to 12v - for i in range(4): - conn = self.btn[i] = imp.Block(DomeButtonConnector()) - pull = self.btn_pull[i] = imp.Block(PullupResistor(10 * kOhm(tol=0.05))) - self.connect(pull.io, conn.sw1, self.mcu.gpio.request(f'btn_sw{i}')) - - self.pwr = self.Block(Ap3012(output_voltage=12*Volt(tol=0.1))) - self.connect(self.v5v, self.pwr.pwr_in) - self.connect(self.gnd, self.pwr.gnd) - self.v12 = self.connect(self.pwr.pwr_out) - with self.implicit_connect( - ImplicitConnect(self.v12, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - for i in range(4): - driver = self.btn_drv[i] = imp.Block(HighSideSwitch(frequency=(0.1, 1) * kHertz)) - self.connect(self.mcu.gpio.request(f'btn_drv{i}'), driver.control) - if i == 0: # only one draws current, since we assume only one will be lit at any point in time - self.connect(driver.output.as_digital_source(), self.btn[i].led_a) - else: - (self.btn_zeroed_current[i],), _ = self.chain( - driver.output.as_digital_source(), - self.Block(ForcedDigitalSinkCurrentDraw((0, 0))), - self.btn[i].led_a) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'spk=24', - 'rgb_red=15', - 'rgb_green=14', - 'rgb_blue=13', - 'sw=27', - 'btn_drv0=5', - 'btn_sw0=6', - 'btn_drv1=7', - 'btn_sw1=8', - 'btn_drv2=9', - 'btn_sw2=10', - 'btn_drv3=11', - 'btn_sw3=12', - ]), - # JLC does not have frequency specs, must be checked TODO - (['pwr', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - - # keep netlist footprints as libraries change - (['btn_drv[0]', 'drv', 'footprint_spec'], 'Package_TO_SOT_SMD:TO-252-2'), - (['btn_drv[1]', 'drv', 'footprint_spec'], ParamValue(['btn_drv[0]', 'drv', 'footprint_spec'])), - (['btn_drv[2]', 'drv', 'footprint_spec'], ParamValue(['btn_drv[0]', 'drv', 'footprint_spec'])), - (['btn_drv[3]', 'drv', 'footprint_spec'], ParamValue(['btn_drv[0]', 'drv', 'footprint_spec'])), - ], - instance_refinements=[ - (['mcu'], Nucleo_F303k8), - (['spk', 'conn'], JstPhKVertical), - ], - class_refinements=[ - (Speaker, ConnectorSpeaker), - ] - ) + @override + def contents(self) -> None: + super().contents() + + self.mcu = self.Block(IoController()) + mcu_pwr = self.mcu.with_mixin(IoControllerPowerOut()) + mcu_usb = self.mcu.with_mixin(IoControllerUsbOut()) + + self.v5v = self.connect(mcu_usb.vusb_out) + self.v3v3 = self.connect(mcu_pwr.pwr_out) + self.gnd = self.connect(self.mcu.gnd) + + with self.implicit_connect( + ImplicitConnect(self.v5v, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.spk_drv, self.spk), _ = self.chain( + self.mcu.with_mixin(IoControllerDac()).dac.request("spk"), imp.Block(Lm4871()), self.Block(Speaker()) + ) + + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.rgb = imp.Block(IndicatorSinkRgbLed()) # status RGB + self.connect(self.mcu.gpio.request_vector("rgb"), self.rgb.signals) + + (self.sw, self.sw_pull), _ = self.chain( + imp.Block(DigitalSwitch()), imp.Block(PullupResistor(10 * kOhm(tol=0.05))), self.mcu.gpio.request("sw") + ) + + self.btn = ElementDict[DomeButtonConnector]() + self.btn_pull = ElementDict[PullupResistor]() + self.btn_drv = ElementDict[HighSideSwitch]() # TODO move to 12v + self.btn_zeroed_current = ElementDict[HighSideSwitch]() # TODO move to 12v + for i in range(4): + conn = self.btn[i] = imp.Block(DomeButtonConnector()) + pull = self.btn_pull[i] = imp.Block(PullupResistor(10 * kOhm(tol=0.05))) + self.connect(pull.io, conn.sw1, self.mcu.gpio.request(f"btn_sw{i}")) + + self.pwr = self.Block(Ap3012(output_voltage=12 * Volt(tol=0.1))) + self.connect(self.v5v, self.pwr.pwr_in) + self.connect(self.gnd, self.pwr.gnd) + self.v12 = self.connect(self.pwr.pwr_out) + with self.implicit_connect( + ImplicitConnect(self.v12, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + for i in range(4): + driver = self.btn_drv[i] = imp.Block(HighSideSwitch(frequency=(0.1, 1) * kHertz)) + self.connect(self.mcu.gpio.request(f"btn_drv{i}"), driver.control) + if i == 0: # only one draws current, since we assume only one will be lit at any point in time + self.connect(driver.output.as_digital_source(), self.btn[i].led_a) + else: + (self.btn_zeroed_current[i],), _ = self.chain( + driver.output.as_digital_source(), + self.Block(ForcedDigitalSinkCurrentDraw((0, 0))), + self.btn[i].led_a, + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "spk=24", + "rgb_red=15", + "rgb_green=14", + "rgb_blue=13", + "sw=27", + "btn_drv0=5", + "btn_sw0=6", + "btn_drv1=7", + "btn_sw1=8", + "btn_drv2=9", + "btn_sw2=10", + "btn_drv3=11", + "btn_sw3=12", + ], + ), + # JLC does not have frequency specs, must be checked TODO + (["pwr", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + # keep netlist footprints as libraries change + (["btn_drv[0]", "drv", "footprint_spec"], "Package_TO_SOT_SMD:TO-252-2"), + (["btn_drv[1]", "drv", "footprint_spec"], ParamValue(["btn_drv[0]", "drv", "footprint_spec"])), + (["btn_drv[2]", "drv", "footprint_spec"], ParamValue(["btn_drv[0]", "drv", "footprint_spec"])), + (["btn_drv[3]", "drv", "footprint_spec"], ParamValue(["btn_drv[0]", "drv", "footprint_spec"])), + ], + instance_refinements=[ + (["mcu"], Nucleo_F303k8), + (["spk", "conn"], JstPhKVertical), + ], + class_refinements=[ + (Speaker, ConnectorSpeaker), + ], + ) class SimonTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(Simon) + def test_design(self) -> None: + compile_board_inplace(Simon) diff --git a/examples/test_swd_debugger.py b/examples/test_swd_debugger.py index 9c5218f8c..8fb14bd3f 100644 --- a/examples/test_swd_debugger.py +++ b/examples/test_swd_debugger.py @@ -6,316 +6,318 @@ class SwdCortexSourceHeader(ProgrammingConnector, FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) # TODO pin at 0v - self.swd = self.Port(SwdTargetPort.empty(), [Input]) - self.reset = self.Port(DigitalSink.empty(), optional=True) - self.swo = self.Port(DigitalBidir.empty(), optional=True) - self.tdi = self.Port(DigitalBidir.empty(), optional=True) - - @override - def contents(self) -> None: - super().contents() - self.conn = self.Block(PinHeader127DualShrouded(10)) - - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - self.connect(self.gnd, self.conn.pins.request('3').adapt_to(Ground()), - self.conn.pins.request('5').adapt_to(Ground()), - self.conn.pins.request('9').adapt_to(Ground())) - self.connect(self.swd.swdio, self.conn.pins.request('2').adapt_to(DigitalBidir())) - self.connect(self.swd.swclk, self.conn.pins.request('4').adapt_to(DigitalSink())) - self.connect(self.swo, self.conn.pins.request('6').adapt_to(DigitalBidir())) - self.connect(self.tdi, self.conn.pins.request('8').adapt_to(DigitalBidir())) - self.connect(self.reset, self.conn.pins.request('10').adapt_to(DigitalSink())) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) # TODO pin at 0v + self.swd = self.Port(SwdTargetPort.empty(), [Input]) + self.reset = self.Port(DigitalSink.empty(), optional=True) + self.swo = self.Port(DigitalBidir.empty(), optional=True) + self.tdi = self.Port(DigitalBidir.empty(), optional=True) + + @override + def contents(self) -> None: + super().contents() + self.conn = self.Block(PinHeader127DualShrouded(10)) + + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + self.connect( + self.gnd, + self.conn.pins.request("3").adapt_to(Ground()), + self.conn.pins.request("5").adapt_to(Ground()), + self.conn.pins.request("9").adapt_to(Ground()), + ) + self.connect(self.swd.swdio, self.conn.pins.request("2").adapt_to(DigitalBidir())) + self.connect(self.swd.swclk, self.conn.pins.request("4").adapt_to(DigitalSink())) + self.connect(self.swo, self.conn.pins.request("6").adapt_to(DigitalBidir())) + self.connect(self.tdi, self.conn.pins.request("8").adapt_to(DigitalBidir())) + self.connect(self.reset, self.conn.pins.request("10").adapt_to(DigitalSink())) class SwdCortexSourceTagConnect(ProgrammingConnector, FootprintBlock): - def __init__(self) -> None: - super().__init__() + def __init__(self) -> None: + super().__init__() - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) # TODO pin at 0v - self.swd = self.Port(SwdTargetPort.empty(), [Input]) - self.reset = self.Port(DigitalSink.empty(), optional=True) - self.swo = self.Port(DigitalBidir.empty(), optional=True) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) # TODO pin at 0v + self.swd = self.Port(SwdTargetPort.empty(), [Input]) + self.reset = self.Port(DigitalSink.empty(), optional=True) + self.swo = self.Port(DigitalBidir.empty(), optional=True) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.conn = self.Block(PinHeader254DualShroudedInline(6)) - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - self.connect(self.swd.swdio, self.conn.pins.request('2').adapt_to(DigitalBidir())) # also TMS - self.connect(self.reset, self.conn.pins.request('3').adapt_to(DigitalSink())) - self.connect(self.swd.swclk, self.conn.pins.request('4').adapt_to(DigitalSink())) - self.connect(self.gnd, self.conn.pins.request('5').adapt_to(Ground())) - self.connect(self.swo, self.conn.pins.request('6').adapt_to(DigitalBidir())) + self.conn = self.Block(PinHeader254DualShroudedInline(6)) + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + self.connect(self.swd.swdio, self.conn.pins.request("2").adapt_to(DigitalBidir())) # also TMS + self.connect(self.reset, self.conn.pins.request("3").adapt_to(DigitalSink())) + self.connect(self.swd.swclk, self.conn.pins.request("4").adapt_to(DigitalSink())) + self.connect(self.gnd, self.conn.pins.request("5").adapt_to(Ground())) + self.connect(self.swo, self.conn.pins.request("6").adapt_to(DigitalBidir())) class SwdSourceBitBang(InternalSubcircuit, Block): - def __init__(self) -> None: - super().__init__() - self.reset_in = self.Port(DigitalSink.empty()) - self.swclk_in = self.Port(DigitalSink.empty()) - self.swdio_in = self.Port(DigitalSink.empty()) # driving side - self.swdio_out = self.Port(DigitalSource.empty()) # target side - self.reset_out = self.Port(DigitalSource.empty()) - self.swo_out = self.Port(DigitalSource.empty()) - - self.swd = self.Port(SwdHostPort.empty(), [Output]) - self.swo_in = self.Port(DigitalSink.empty()) - - @override - def contents(self) -> None: - super().contents() - - self.swclk_res = self.Block(Resistor(resistance=22*Ohm(tol=0.05))) - self.swdio_res = self.Block(Resistor(resistance=22*Ohm(tol=0.05))) - self.swdio_drv_res = self.Block(Resistor(resistance=100*Ohm(tol=0.05))) - - self.reset_res = self.Block(Resistor(resistance=22*Ohm(tol=0.05))) - self.swo_res = self.Block(Resistor(resistance=22*Ohm(tol=0.05))) - - self.connect(self.swclk_res.a.adapt_to(DigitalSink()), self.swclk_in) - self.connect(self.swclk_res.b.adapt_to(DigitalSource()), self.swd.swclk) - self.connect(self.swdio_drv_res.a.adapt_to(DigitalSink()), self.swdio_in) - self.connect(self.swdio_res.a.adapt_to(DigitalBidir()), - self.swdio_drv_res.b.adapt_to(DigitalBidir()), - self.swdio_out) - self.connect(self.swdio_res.b.adapt_to(DigitalSink()), self.swd.swdio) - - self.connect(self.reset_res.a.adapt_to(DigitalSink()), self.reset_in) - self.connect(self.reset_res.b.adapt_to(DigitalSource()), self.reset_out) - self.connect(self.swo_res.a.adapt_to(DigitalSource()), self.swo_out) - self.connect(self.swo_res.b.adapt_to(DigitalSink()), self.swo_in) + def __init__(self) -> None: + super().__init__() + self.reset_in = self.Port(DigitalSink.empty()) + self.swclk_in = self.Port(DigitalSink.empty()) + self.swdio_in = self.Port(DigitalSink.empty()) # driving side + self.swdio_out = self.Port(DigitalSource.empty()) # target side + self.reset_out = self.Port(DigitalSource.empty()) + self.swo_out = self.Port(DigitalSource.empty()) + + self.swd = self.Port(SwdHostPort.empty(), [Output]) + self.swo_in = self.Port(DigitalSink.empty()) + + @override + def contents(self) -> None: + super().contents() + + self.swclk_res = self.Block(Resistor(resistance=22 * Ohm(tol=0.05))) + self.swdio_res = self.Block(Resistor(resistance=22 * Ohm(tol=0.05))) + self.swdio_drv_res = self.Block(Resistor(resistance=100 * Ohm(tol=0.05))) + + self.reset_res = self.Block(Resistor(resistance=22 * Ohm(tol=0.05))) + self.swo_res = self.Block(Resistor(resistance=22 * Ohm(tol=0.05))) + + self.connect(self.swclk_res.a.adapt_to(DigitalSink()), self.swclk_in) + self.connect(self.swclk_res.b.adapt_to(DigitalSource()), self.swd.swclk) + self.connect(self.swdio_drv_res.a.adapt_to(DigitalSink()), self.swdio_in) + self.connect( + self.swdio_res.a.adapt_to(DigitalBidir()), self.swdio_drv_res.b.adapt_to(DigitalBidir()), self.swdio_out + ) + self.connect(self.swdio_res.b.adapt_to(DigitalSink()), self.swd.swdio) + + self.connect(self.reset_res.a.adapt_to(DigitalSink()), self.reset_in) + self.connect(self.reset_res.b.adapt_to(DigitalSource()), self.reset_out) + self.connect(self.swo_res.a.adapt_to(DigitalSource()), self.swo_out) + self.connect(self.swo_res.b.adapt_to(DigitalSink()), self.swo_in) class SwdDebugger(JlcBoardTop): - @override - def contents(self) -> None: - super().contents() - - self.usb = self.Block(UsbCReceptacle()) - - self.vusb = self.connect(self.usb.pwr) - self.gnd = self.connect(self.usb.gnd) - - with self.implicit_connect( - ImplicitConnect(self.vusb, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6)*Volt)) - - self.usb_reg = imp.Block(VoltageRegulator(3.3*Volt(tol=0.05))) - self.v3v3 = self.connect(self.usb_reg.pwr_out) - - self.target_reg = imp.Block(VoltageRegulator(3.3*Volt(tol=0.05))) - self.vtarget = self.connect(self.target_reg.pwr_out) - - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) - - (self.led_tgt, ), _ = self.chain(self.mcu.gpio.request(f'led_target'), - imp.Block(IndicatorLed(Led.Yellow))) - (self.led_usb, ), _ = self.chain(self.mcu.gpio.request(f'led_usb'), - imp.Block(IndicatorLed(Led.White))) - - (self.en_pull, ), _ = self.chain(self.mcu.gpio.request('target_reg_en'), - imp.Block(PullupResistor(4.7*kOhm(tol=0.1))), - self.target_reg.with_mixin(Resettable()).reset) - - self.target_drv = imp.Block(SwdSourceBitBang()) - self.connect(self.mcu.gpio.request('target_swclk'), self.target_drv.swclk_in) # TODO BMP uses pin 15 - self.connect(self.mcu.gpio.request('target_swdio_out'), self.target_drv.swdio_out) - self.connect(self.mcu.gpio.request('target_swdio_in'), self.target_drv.swdio_in) - self.connect(self.mcu.gpio.request('target_reset'), self.target_drv.reset_in) - self.reset_pull = imp.Block(PullupResistor(10*kOhm(tol=0.05))).connected(io=self.target_drv.reset_in) - self.reset_sw = imp.Block(DigitalSwitch()) - self.connect(self.reset_sw.out, self.target_drv.reset_in) - self.connect(self.mcu.gpio.request('target_swo'), self.target_drv.swo_out) - - with self.implicit_connect( + @override + def contents(self) -> None: + super().contents() + + self.usb = self.Block(UsbCReceptacle()) + + self.vusb = self.connect(self.usb.pwr) + self.gnd = self.connect(self.usb.gnd) + + with self.implicit_connect( + ImplicitConnect(self.vusb, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6) * Volt)) + + self.usb_reg = imp.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.v3v3 = self.connect(self.usb_reg.pwr_out) + + self.target_reg = imp.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.vtarget = self.connect(self.target_reg.pwr_out) + + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + (self.led_tgt,), _ = self.chain(self.mcu.gpio.request(f"led_target"), imp.Block(IndicatorLed(Led.Yellow))) + (self.led_usb,), _ = self.chain(self.mcu.gpio.request(f"led_usb"), imp.Block(IndicatorLed(Led.White))) + + (self.en_pull,), _ = self.chain( + self.mcu.gpio.request("target_reg_en"), + imp.Block(PullupResistor(4.7 * kOhm(tol=0.1))), + self.target_reg.with_mixin(Resettable()).reset, + ) + + self.target_drv = imp.Block(SwdSourceBitBang()) + self.connect(self.mcu.gpio.request("target_swclk"), self.target_drv.swclk_in) # TODO BMP uses pin 15 + self.connect(self.mcu.gpio.request("target_swdio_out"), self.target_drv.swdio_out) + self.connect(self.mcu.gpio.request("target_swdio_in"), self.target_drv.swdio_in) + self.connect(self.mcu.gpio.request("target_reset"), self.target_drv.reset_in) + self.reset_pull = imp.Block(PullupResistor(10 * kOhm(tol=0.05))).connected(io=self.target_drv.reset_in) + self.reset_sw = imp.Block(DigitalSwitch()) + self.connect(self.reset_sw.out, self.target_drv.reset_in) + self.connect(self.mcu.gpio.request("target_swo"), self.target_drv.swo_out) + + with self.implicit_connect( ImplicitConnect(self.vtarget, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.target = imp.Block(SwdCortexSourceHeader()) - self.connect(self.target_drv.swd, self.target.swd) - self.connect(self.target_drv.swo_in, self.target.swo) - self.connect(self.target_drv.reset_out, self.target.reset) - - self.led_target = imp.Block(VoltageIndicatorLed(Led.Green)) - (self.target_sense, ), _ = self.chain( - self.vtarget, - imp.Block(VoltageDivider(output_voltage=1.65*Volt(tol=0.05), impedance=4.7/2*kOhm(tol=0.05))), - self.mcu.adc.request('target_vsense') - ) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Stm32f103_48), - (['mcu', 'crystal'], Cstne), - (['usb_reg'], Ap2204k), - (['target_reg'], Ap2204k), - ], - instance_values=[ - (['refdes_prefix'], 'S'), # unique refdes for panelization - (['mcu', 'crystal', 'frequency'], Range.from_tolerance(8000000, 0.005)), - (['mcu', 'pin_assigns'], [ - # these are pinnings on stock st-link - 'target_vsense=PA0', - 'target_swclk=PB13', - 'target_swdio_out=PB14', - 'target_swdio_in=PB12', - 'target_reset=PB0', - 'target_swo=PA10', - 'led_target=PA9', - 'led_usb=PB6', # CONNECTED_LED in DAPLink, LED_DAP_BLUE in F103 mbed HDK - 'target_reg_en=PB15', # POWER_EN in DAPLink - ]), - - # 2.2uF generates a 1206, but 4.7uF allows a 0805 - (['usb_reg', 'out_cap', 'cap', 'capacitance'], Range.from_tolerance(4.7e-6, 0.2)), - (['target_reg', 'out_cap', 'cap', 'capacitance'], Range.from_tolerance(4.7e-6, 0.2)), - - (['mcu', 'swd_swo_pin'], 'PB3'), - ], - class_values=[ - (SelectorArea, ['footprint_area'], Range.from_lower(1.5)), # at least 0402 - ], - ) + ) as imp: + self.target = imp.Block(SwdCortexSourceHeader()) + self.connect(self.target_drv.swd, self.target.swd) + self.connect(self.target_drv.swo_in, self.target.swo) + self.connect(self.target_drv.reset_out, self.target.reset) + + self.led_target = imp.Block(VoltageIndicatorLed(Led.Green)) + (self.target_sense,), _ = self.chain( + self.vtarget, + imp.Block(VoltageDivider(output_voltage=1.65 * Volt(tol=0.05), impedance=4.7 / 2 * kOhm(tol=0.05))), + self.mcu.adc.request("target_vsense"), + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Stm32f103_48), + (["mcu", "crystal"], Cstne), + (["usb_reg"], Ap2204k), + (["target_reg"], Ap2204k), + ], + instance_values=[ + (["refdes_prefix"], "S"), # unique refdes for panelization + (["mcu", "crystal", "frequency"], Range.from_tolerance(8000000, 0.005)), + ( + ["mcu", "pin_assigns"], + [ + # these are pinnings on stock st-link + "target_vsense=PA0", + "target_swclk=PB13", + "target_swdio_out=PB14", + "target_swdio_in=PB12", + "target_reset=PB0", + "target_swo=PA10", + "led_target=PA9", + "led_usb=PB6", # CONNECTED_LED in DAPLink, LED_DAP_BLUE in F103 mbed HDK + "target_reg_en=PB15", # POWER_EN in DAPLink + ], + ), + # 2.2uF generates a 1206, but 4.7uF allows a 0805 + (["usb_reg", "out_cap", "cap", "capacitance"], Range.from_tolerance(4.7e-6, 0.2)), + (["target_reg", "out_cap", "cap", "capacitance"], Range.from_tolerance(4.7e-6, 0.2)), + (["mcu", "swd_swo_pin"], "PB3"), + ], + class_values=[ + (SelectorArea, ["footprint_area"], Range.from_lower(1.5)), # at least 0402 + ], + ) class SwdSourceBitBangDirect(InternalSubcircuit, Block): - def __init__(self) -> None: - super().__init__() - self.swclk = self.Port(DigitalSink.empty()) - self.swdio = self.Port(DigitalSink.empty()) - self.swd = self.Port(SwdHostPort.empty(), [Output]) + def __init__(self) -> None: + super().__init__() + self.swclk = self.Port(DigitalSink.empty()) + self.swdio = self.Port(DigitalSink.empty()) + self.swd = self.Port(SwdHostPort.empty(), [Output]) - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.connect(self.swclk, self.swd.swclk) - self.connect(self.swdio, self.swd.swdio) + self.connect(self.swclk, self.swd.swclk) + self.connect(self.swdio, self.swd.swdio) class PicoProbe(JlcBoardTop): - @override - def contents(self) -> None: - super().contents() + @override + def contents(self) -> None: + super().contents() - self.usb = self.Block(UsbCReceptacle()) + self.usb = self.Block(UsbCReceptacle()) - self.vusb = self.connect(self.usb.pwr) - self.gnd = self.connect(self.usb.gnd) + self.vusb = self.connect(self.usb.pwr) + self.gnd = self.connect(self.usb.gnd) - with self.implicit_connect( + with self.implicit_connect( ImplicitConnect(self.vusb, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6)*Volt)) + ) as imp: + self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6) * Volt)) - self.usb_reg = imp.Block(VoltageRegulator(3.3*Volt(tol=0.05))) - self.v3v3 = self.connect(self.usb_reg.pwr_out) + self.usb_reg = imp.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.v3v3 = self.connect(self.usb_reg.pwr_out) - self.target_reg = imp.Block(VoltageRegulator(3.3*Volt(tol=0.05))) - self.vtarget = self.connect(self.target_reg.pwr_out) + self.target_reg = imp.Block(VoltageRegulator(3.3 * Volt(tol=0.05))) + self.vtarget = self.connect(self.target_reg.pwr_out) - with self.implicit_connect( + with self.implicit_connect( ImplicitConnect(self.v3v3, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) + ) as imp: + self.mcu = imp.Block(IoController()) - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) - (self.led_tgt, ), _ = self.chain(self.mcu.gpio.request(f'led_target'), - imp.Block(IndicatorLed(Led.Yellow))) - (self.led_usb, ), _ = self.chain(self.mcu.gpio.request(f'led_usb'), - imp.Block(IndicatorLed(Led.White))) + (self.led_tgt,), _ = self.chain(self.mcu.gpio.request(f"led_target"), imp.Block(IndicatorLed(Led.Yellow))) + (self.led_usb,), _ = self.chain(self.mcu.gpio.request(f"led_usb"), imp.Block(IndicatorLed(Led.White))) - (self.en_pull, ), _ = self.chain(self.mcu.gpio.request('target_reg_en'), - imp.Block(PullupResistor(4.7*kOhm(tol=0.1))), - self.target_reg.with_mixin(Resettable()).reset) + (self.en_pull,), _ = self.chain( + self.mcu.gpio.request("target_reg_en"), + imp.Block(PullupResistor(4.7 * kOhm(tol=0.1))), + self.target_reg.with_mixin(Resettable()).reset, + ) - self.target_drv = imp.Block(SwdSourceBitBangDirect()) - self.connect(self.mcu.gpio.request('target_swclk'), self.target_drv.swclk) - self.connect(self.mcu.gpio.request('target_swdio'), self.target_drv.swdio) + self.target_drv = imp.Block(SwdSourceBitBangDirect()) + self.connect(self.mcu.gpio.request("target_swclk"), self.target_drv.swclk) + self.connect(self.mcu.gpio.request("target_swdio"), self.target_drv.swdio) - with self.implicit_connect( + with self.implicit_connect( ImplicitConnect(self.vtarget, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.target = imp.Block(SwdCortexSourceTagConnect()) - self.connect(self.target_drv.swd, self.target.swd) - self.connect(self.mcu.gpio.request('target_reset'), self.target.swo) - self.connect(self.mcu.gpio.request('target_swo'), self.target.reset) - - self.led_target = imp.Block(VoltageIndicatorLed(Led.Green)) - (self.target_sense, ), _ = self.chain( - self.vtarget, - imp.Block(VoltageDivider(output_voltage=1.65*Volt(tol=0.05), impedance=4.7/2*kOhm(tol=0.05))), - self.mcu.adc.request('target_vsense') - ) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Rp2040), - (['mcu', 'crystal'], Cstne), - (['usb_reg'], Ap2204k), - (['target_reg'], Ap2204k), - ], - instance_values=[ - (['refdes_prefix'], 'S'), # unique refdes for panelization - (['mcu', 'pin_assigns'], [ - # from https://github.com/raspberrypi/picoprobe/blob/master/include/board_pico_config.h - 'target_swclk=GPIO2', - 'target_swdio=GPIO3', - 'target_reset=GPIO1', # disabled by default - 'target_swo=GPIO5', # UART RX - 'led_usb=GPIO25', # aka Pico internal LED - - # from https://github.com/raspberrypi/picoprobe/blob/master/include/board_example_config.h - 'led_target=GPIO16', # DAP_RUNNING_LED - - # others - 'target_vsense=GPIO28', - 'target_reg_en=17', - ]), - - # 2.2uF generates a 1206, but 4.7uF allows a 0805 - (['usb_reg', 'out_cap', 'cap', 'capacitance'], Range.from_tolerance(4.7e-6, 0.2)), - (['target_reg', 'out_cap', 'cap', 'capacitance'], Range.from_tolerance(4.7e-6, 0.2)), - - (['mcu', 'swd_swo_pin'], 'GPIO12'), # arbitrary closest UART TX - ], - class_refinements=[ - (SwdCortexTargetHeader, SwdCortexTargetTagConnect), - (TagConnect, TagConnectNonLegged), - ], - class_values=[ - (SelectorArea, ['footprint_area'], Range.from_lower(1.5)), # at least 0402 - ], - ) + ) as imp: + self.target = imp.Block(SwdCortexSourceTagConnect()) + self.connect(self.target_drv.swd, self.target.swd) + self.connect(self.mcu.gpio.request("target_reset"), self.target.swo) + self.connect(self.mcu.gpio.request("target_swo"), self.target.reset) + + self.led_target = imp.Block(VoltageIndicatorLed(Led.Green)) + (self.target_sense,), _ = self.chain( + self.vtarget, + imp.Block(VoltageDivider(output_voltage=1.65 * Volt(tol=0.05), impedance=4.7 / 2 * kOhm(tol=0.05))), + self.mcu.adc.request("target_vsense"), + ) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Rp2040), + (["mcu", "crystal"], Cstne), + (["usb_reg"], Ap2204k), + (["target_reg"], Ap2204k), + ], + instance_values=[ + (["refdes_prefix"], "S"), # unique refdes for panelization + ( + ["mcu", "pin_assigns"], + [ + # from https://github.com/raspberrypi/picoprobe/blob/master/include/board_pico_config.h + "target_swclk=GPIO2", + "target_swdio=GPIO3", + "target_reset=GPIO1", # disabled by default + "target_swo=GPIO5", # UART RX + "led_usb=GPIO25", # aka Pico internal LED + # from https://github.com/raspberrypi/picoprobe/blob/master/include/board_example_config.h + "led_target=GPIO16", # DAP_RUNNING_LED + # others + "target_vsense=GPIO28", + "target_reg_en=17", + ], + ), + # 2.2uF generates a 1206, but 4.7uF allows a 0805 + (["usb_reg", "out_cap", "cap", "capacitance"], Range.from_tolerance(4.7e-6, 0.2)), + (["target_reg", "out_cap", "cap", "capacitance"], Range.from_tolerance(4.7e-6, 0.2)), + (["mcu", "swd_swo_pin"], "GPIO12"), # arbitrary closest UART TX + ], + class_refinements=[ + (SwdCortexTargetHeader, SwdCortexTargetTagConnect), + (TagConnect, TagConnectNonLegged), + ], + class_values=[ + (SelectorArea, ["footprint_area"], Range.from_lower(1.5)), # at least 0402 + ], + ) class SwdDebuggerTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(SwdDebugger) - def test_picoprobe(self) -> None: - compile_board_inplace(PicoProbe) + def test_design(self) -> None: + compile_board_inplace(SwdDebugger) + + def test_picoprobe(self) -> None: + compile_board_inplace(PicoProbe) diff --git a/examples/test_tofarray.py b/examples/test_tofarray.py index 441606222..9a66c28b2 100644 --- a/examples/test_tofarray.py +++ b/examples/test_tofarray.py @@ -6,161 +6,168 @@ class CanConnector(Connector): - def __init__(self) -> None: - super().__init__() - - self.pwr = self.Port(VoltageSource.empty(), optional=True) - self.gnd = self.Port(Ground.empty()) - self.differential = self.Port(CanDiffPort.empty(), [Output]) - - self.conn = self.Block(PassiveConnector()) - self.connect(self.pwr, self.conn.pins.request('2').adapt_to(VoltageSource( - voltage_out=(7, 14) * Volt, # TODO get limits from CAN power brick? - current_limits=(0, 0.15) * Amp # TODO get actual limits from ??? - ))) - self.connect(self.gnd, self.conn.pins.request('3').adapt_to(Ground())) - self.connect(self.differential.canh, self.conn.pins.request('4').adapt_to(DigitalSource())) - self.connect(self.differential.canl, self.conn.pins.request('5').adapt_to(DigitalSource())) + def __init__(self) -> None: + super().__init__() + + self.pwr = self.Port(VoltageSource.empty(), optional=True) + self.gnd = self.Port(Ground.empty()) + self.differential = self.Port(CanDiffPort.empty(), [Output]) + + self.conn = self.Block(PassiveConnector()) + self.connect( + self.pwr, + self.conn.pins.request("2").adapt_to( + VoltageSource( + voltage_out=(7, 14) * Volt, # TODO get limits from CAN power brick? + current_limits=(0, 0.15) * Amp, # TODO get actual limits from ??? + ) + ), + ) + self.connect(self.gnd, self.conn.pins.request("3").adapt_to(Ground())) + self.connect(self.differential.canh, self.conn.pins.request("4").adapt_to(DigitalSource())) + self.connect(self.differential.canl, self.conn.pins.request("5").adapt_to(DigitalSource())) class TofArray(JlcBoardTop): - """A ToF LiDAR array with application as emulating a laser harp and demonstrating another array topology. - """ - def __init__(self) -> None: - super().__init__() - - # design configuration variables - self.tof_count = self.Parameter(IntExpr(5)) - - @override - def contents(self) -> None: - super().contents() - - self.usb = self.Block(UsbCReceptacle()) - self.can = self.Block(CanConnector()) - - self.vusb = self.connect(self.usb.pwr) - self.gnd = self.connect(self.usb.gnd, self.can.gnd) - - self.tp_vusb = self.Block(VoltageTestPoint()).connected(self.usb.pwr) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( - self.vusb, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint()), - imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9)*Volt)) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - (self.sw1, ), self.sw1_chain = self.chain( - imp.Block(DigitalSwitch()), self.mcu.gpio.request('sw1')) - # realistically it would be cleaner for the RGB to be separate, but this demonstrates packing - (self.leds, ), self.leds_chain = self.chain( - imp.Block(IndicatorSinkLedArray(self.tof_count + 3)), self.mcu.gpio.request_vector('leds')) - - self.tof = imp.Block(Vl53l0xArray(self.tof_count)) - (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( - self.mcu.i2c.request('i2c'), - imp.Block(I2cPullup()), imp.Block(I2cTestPoint()), - self.tof.i2c) - self.connect(self.mcu.gpio.request_vector('tof_reset'), self.tof.reset) - - (self.usb_esd, ), self.usb_chain = self.chain( - self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) - - (self.tp_can, self.xcvr, self.can_esd), self.can_chain = self.chain( - self.mcu.with_mixin(IoControllerCan()).can.request('can'), - imp.Block(CanControllerTestPoint()), - imp.Block(Sn65hvd230()), imp.Block(CanEsdDiode()), self.can.differential - ) - - # 5V DOMAIN - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.tp_spk, self.spk_dac, self.tp_spk_in, self.spk_drv, self.spk), self.spk_chain = self.chain( - self.mcu.gpio.request('spk'), - imp.Block(DigitalTestPoint()), - imp.Block(LowPassRcDac(1*kOhm(tol=0.05), 5*kHertz(tol=0.5))), - imp.Block(AnalogTestPoint()), - imp.Block(Tpa2005d1(gain=Range.from_tolerance(10, 0.2))), - self.Block(Speaker())) - - # limit the power draw of the speaker to not overcurrent the USB source - # this indicates that the device will only be run at partial power - (self.spk_pwr, ), _ = self.chain( - self.vusb, - self.Block(ForcedVoltageCurrentDraw((0, 0.05)*Amp)), - self.spk_drv.pwr - ) - - @override - def multipack(self) -> None: - self.res1 = self.PackedBlock(ResistorArray()) - self.pack(self.res1.elements.request('0'), ['leds', 'led[0]', 'res']) - self.pack(self.res1.elements.request('1'), ['leds', 'led[1]', 'res']) - self.pack(self.res1.elements.request('2'), ['rgb', 'device', 'red_res']) - self.pack(self.res1.elements.request('3'), ['rgb', 'device', 'green_res']) - - self.res2 = self.PackedBlock(ResistorArray()) - self.pack(self.res2.elements.request('0'), ['rgb', 'device', 'blue_res']) - self.pack(self.res2.elements.request('1'), ['leds', 'led[2]', 'res']) - self.pack(self.res2.elements.request('2'), ['leds', 'led[3]', 'res']) - self.pack(self.res2.elements.request('3'), ['leds', 'led[4]', 'res']) - - self.rgb = self.PackedBlock(IndicatorSinkPackedRgbLed()) - self.pack(self.rgb.red, ['leds', 'led[5]']) - self.pack(self.rgb.green, ['leds', 'led[6]']) - self.pack(self.rgb.blue, ['leds', 'led[7]']) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Stm32f103_48), - (['reg_3v3'], Ldl1117), # TBD find one that is in stock - (['spk', 'conn'], JstPhKVertical), - (['can', 'conn'], MolexSl), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'spk=11', # PWMable pin, with TIMx_CHx function - 'sw1=19', - 'leds_0=20', - 'leds_1=25', - 'leds_5=26', # RGB R - 'leds_6=27', # RGB G - 'leds_7=28', # RGB B - 'leds_2=29', - 'leds_3=30', - 'leds_4=31', - 'tof_reset_0=42', - 'tof_reset_1=41', - 'tof_reset_2=4', - 'tof_reset_3=3', - 'tof_reset_4=2', - ]), - (['mcu', 'swd_swo_pin'], 'PB3'), - ], - class_refinements=[ - (SwdCortexTargetConnector, SwdCortexTargetTc2050), - (Speaker, ConnectorSpeaker), - ], - ) + """A ToF LiDAR array with application as emulating a laser harp and demonstrating another array topology.""" + + def __init__(self) -> None: + super().__init__() + + # design configuration variables + self.tof_count = self.Parameter(IntExpr(5)) + + @override + def contents(self) -> None: + super().contents() + + self.usb = self.Block(UsbCReceptacle()) + self.can = self.Block(CanConnector()) + + self.vusb = self.connect(self.usb.pwr) + self.gnd = self.connect(self.usb.gnd, self.can.gnd) + + self.tp_vusb = self.Block(VoltageTestPoint()).connected(self.usb.pwr) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.reg_3v3, self.tp_3v3, self.prot_3v3), _ = self.chain( + self.vusb, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint()), + imp.Block(ProtectionZenerDiode(voltage=(3.45, 3.9) * Volt)), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.mcu = imp.Block(IoController()) + + (self.sw1,), self.sw1_chain = self.chain(imp.Block(DigitalSwitch()), self.mcu.gpio.request("sw1")) + # realistically it would be cleaner for the RGB to be separate, but this demonstrates packing + (self.leds,), self.leds_chain = self.chain( + imp.Block(IndicatorSinkLedArray(self.tof_count + 3)), self.mcu.gpio.request_vector("leds") + ) + + self.tof = imp.Block(Vl53l0xArray(self.tof_count)) + (self.i2c_pull, self.i2c_tp), self.i2c_chain = self.chain( + self.mcu.i2c.request("i2c"), imp.Block(I2cPullup()), imp.Block(I2cTestPoint()), self.tof.i2c + ) + self.connect(self.mcu.gpio.request_vector("tof_reset"), self.tof.reset) + + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + (self.tp_can, self.xcvr, self.can_esd), self.can_chain = self.chain( + self.mcu.with_mixin(IoControllerCan()).can.request("can"), + imp.Block(CanControllerTestPoint()), + imp.Block(Sn65hvd230()), + imp.Block(CanEsdDiode()), + self.can.differential, + ) + + # 5V DOMAIN + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + (self.tp_spk, self.spk_dac, self.tp_spk_in, self.spk_drv, self.spk), self.spk_chain = self.chain( + self.mcu.gpio.request("spk"), + imp.Block(DigitalTestPoint()), + imp.Block(LowPassRcDac(1 * kOhm(tol=0.05), 5 * kHertz(tol=0.5))), + imp.Block(AnalogTestPoint()), + imp.Block(Tpa2005d1(gain=Range.from_tolerance(10, 0.2))), + self.Block(Speaker()), + ) + + # limit the power draw of the speaker to not overcurrent the USB source + # this indicates that the device will only be run at partial power + (self.spk_pwr,), _ = self.chain( + self.vusb, self.Block(ForcedVoltageCurrentDraw((0, 0.05) * Amp)), self.spk_drv.pwr + ) + + @override + def multipack(self) -> None: + self.res1 = self.PackedBlock(ResistorArray()) + self.pack(self.res1.elements.request("0"), ["leds", "led[0]", "res"]) + self.pack(self.res1.elements.request("1"), ["leds", "led[1]", "res"]) + self.pack(self.res1.elements.request("2"), ["rgb", "device", "red_res"]) + self.pack(self.res1.elements.request("3"), ["rgb", "device", "green_res"]) + + self.res2 = self.PackedBlock(ResistorArray()) + self.pack(self.res2.elements.request("0"), ["rgb", "device", "blue_res"]) + self.pack(self.res2.elements.request("1"), ["leds", "led[2]", "res"]) + self.pack(self.res2.elements.request("2"), ["leds", "led[3]", "res"]) + self.pack(self.res2.elements.request("3"), ["leds", "led[4]", "res"]) + + self.rgb = self.PackedBlock(IndicatorSinkPackedRgbLed()) + self.pack(self.rgb.red, ["leds", "led[5]"]) + self.pack(self.rgb.green, ["leds", "led[6]"]) + self.pack(self.rgb.blue, ["leds", "led[7]"]) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Stm32f103_48), + (["reg_3v3"], Ldl1117), # TBD find one that is in stock + (["spk", "conn"], JstPhKVertical), + (["can", "conn"], MolexSl), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "spk=11", # PWMable pin, with TIMx_CHx function + "sw1=19", + "leds_0=20", + "leds_1=25", + "leds_5=26", # RGB R + "leds_6=27", # RGB G + "leds_7=28", # RGB B + "leds_2=29", + "leds_3=30", + "leds_4=31", + "tof_reset_0=42", + "tof_reset_1=41", + "tof_reset_2=4", + "tof_reset_3=3", + "tof_reset_4=2", + ], + ), + (["mcu", "swd_swo_pin"], "PB3"), + ], + class_refinements=[ + (SwdCortexTargetConnector, SwdCortexTargetTc2050), + (Speaker, ConnectorSpeaker), + ], + ) class TofArrayTestTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(TofArray) + def test_design(self) -> None: + compile_board_inplace(TofArray) diff --git a/examples/test_usb_fpga_programmer.py b/examples/test_usb_fpga_programmer.py index ec456c09f..71cae372f 100644 --- a/examples/test_usb_fpga_programmer.py +++ b/examples/test_usb_fpga_programmer.py @@ -6,76 +6,81 @@ class FpgaProgrammingHeader(Connector, Block): - """Custom programming header for iCE40 loosely based on the SWD pinning""" - def __init__(self) -> None: - super().__init__() - self.pwr = self.Port(VoltageSink.empty(), optional=True) - self.gnd = self.Port(Ground.empty(), [Common]) - self.spi = self.Port(SpiPeripheral.empty()) - self.cs = self.Port(DigitalSink.empty()) - self.reset = self.Port(DigitalSink.empty()) - - @override - def contents(self) -> None: - super().contents() - self.conn = self.Block(PinHeader127DualShrouded(10)) - self.connect(self.pwr, self.conn.pins.request('1').adapt_to(VoltageSink())) - self.connect(self.gnd, self.conn.pins.request('3').adapt_to(Ground()), - self.conn.pins.request('5').adapt_to(Ground()), - self.conn.pins.request('9').adapt_to(Ground())) - self.connect(self.cs, self.conn.pins.request('2').adapt_to(DigitalSink())) # swd: swdio - self.connect(self.spi.sck, self.conn.pins.request('4').adapt_to(DigitalSink())) # swd: swclk - self.connect(self.spi.mosi, self.conn.pins.request('6').adapt_to(DigitalSink())) # swd: swo - self.connect(self.spi.miso, self.conn.pins.request('8').adapt_to(DigitalSource())) # swd: NC or jtag: tdi - self.connect(self.reset, self.conn.pins.request('10').adapt_to(DigitalSink())) + """Custom programming header for iCE40 loosely based on the SWD pinning""" + + def __init__(self) -> None: + super().__init__() + self.pwr = self.Port(VoltageSink.empty(), optional=True) + self.gnd = self.Port(Ground.empty(), [Common]) + self.spi = self.Port(SpiPeripheral.empty()) + self.cs = self.Port(DigitalSink.empty()) + self.reset = self.Port(DigitalSink.empty()) + + @override + def contents(self) -> None: + super().contents() + self.conn = self.Block(PinHeader127DualShrouded(10)) + self.connect(self.pwr, self.conn.pins.request("1").adapt_to(VoltageSink())) + self.connect( + self.gnd, + self.conn.pins.request("3").adapt_to(Ground()), + self.conn.pins.request("5").adapt_to(Ground()), + self.conn.pins.request("9").adapt_to(Ground()), + ) + self.connect(self.cs, self.conn.pins.request("2").adapt_to(DigitalSink())) # swd: swdio + self.connect(self.spi.sck, self.conn.pins.request("4").adapt_to(DigitalSink())) # swd: swclk + self.connect(self.spi.mosi, self.conn.pins.request("6").adapt_to(DigitalSink())) # swd: swo + self.connect(self.spi.miso, self.conn.pins.request("8").adapt_to(DigitalSource())) # swd: NC or jtag: tdi + self.connect(self.reset, self.conn.pins.request("10").adapt_to(DigitalSink())) class UsbFpgaProgrammer(JlcBoardTop): - """USB UART converter board""" - @override - def contents(self) -> None: - super().contents() - self.usb = self.Block(UsbCReceptacle()) - - self.vusb = self.connect(self.usb.pwr) - self.gnd = self.connect(self.usb.gnd) - - # POWER - with self.implicit_connect( - ImplicitConnect(self.vusb, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - # since USB is 5.25 max, we can't use the 5.2v Zener that is a basic part =( - self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6)*Volt)) - - self.ft232 = imp.Block(Ft232hl()) - (self.usb_esd, ), self.usb_chain = self.chain( - self.usb.usb, imp.Block(UsbEsdDiode()), self.ft232.usb) - (self.led0, ), _ = self.chain(self.ft232.acbus.request('0'), imp.Block(IndicatorLed(Led.White))) # TXDEN - (self.led1, ), _ = self.chain(self.ft232.acbus.request('3'), imp.Block(IndicatorLed(Led.Green))) # RXLED - (self.led2, ), _ = self.chain(self.ft232.acbus.request('4'), imp.Block(IndicatorLed(Led.Yellow))) # TXLED - - self.out = imp.Block(FpgaProgrammingHeader()) - self.connect(self.ft232.mpsse, self.out.spi) - self.connect(self.ft232.adbus.request('4'), self.out.cs) - self.connect(self.ft232.adbus.request('7'), self.out.reset) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - ], - instance_values=[ - (['refdes_prefix'], 'F'), # unique refdes for panelization - ], - class_refinements=[ - (UsbEsdDiode, Pgb102st23), # as recommended by the FT232H datasheet, also for the weird "sot-23" package - ], - class_values=[ - ], - ) + """USB UART converter board""" + + @override + def contents(self) -> None: + super().contents() + self.usb = self.Block(UsbCReceptacle()) + + self.vusb = self.connect(self.usb.pwr) + self.gnd = self.connect(self.usb.gnd) + + # POWER + with self.implicit_connect( + ImplicitConnect(self.vusb, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + # since USB is 5.25 max, we can't use the 5.2v Zener that is a basic part =( + self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6) * Volt)) + + self.ft232 = imp.Block(Ft232hl()) + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.ft232.usb) + (self.led0,), _ = self.chain(self.ft232.acbus.request("0"), imp.Block(IndicatorLed(Led.White))) # TXDEN + (self.led1,), _ = self.chain(self.ft232.acbus.request("3"), imp.Block(IndicatorLed(Led.Green))) # RXLED + (self.led2,), _ = self.chain(self.ft232.acbus.request("4"), imp.Block(IndicatorLed(Led.Yellow))) # TXLED + + self.out = imp.Block(FpgaProgrammingHeader()) + self.connect(self.ft232.mpsse, self.out.spi) + self.connect(self.ft232.adbus.request("4"), self.out.cs) + self.connect(self.ft232.adbus.request("7"), self.out.reset) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[], + instance_values=[ + (["refdes_prefix"], "F"), # unique refdes for panelization + ], + class_refinements=[ + ( + UsbEsdDiode, + Pgb102st23, + ), # as recommended by the FT232H datasheet, also for the weird "sot-23" package + ], + class_values=[], + ) class UsbFpgaProgrammerTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(UsbFpgaProgrammer) + def test_design(self) -> None: + compile_board_inplace(UsbFpgaProgrammer) diff --git a/examples/test_usb_key.py b/examples/test_usb_key.py index 2563cb228..03bed1a34 100644 --- a/examples/test_usb_key.py +++ b/examples/test_usb_key.py @@ -6,122 +6,128 @@ class StTscSenseChannel(Block): - """Sense channel for STM micros' TSC peripheral.""" - def __init__(self) -> None: - super().__init__() - self.io = self.Port(DigitalBidir.empty(), [Input]) + """Sense channel for STM micros' TSC peripheral.""" - @override - def contents(self) -> None: - super().contents() - self.res = self.Block(Resistor(resistance=10*kOhm(tol=0.05))) # recommended by ST - self.connect(self.io, self.res.a.adapt_to(DigitalBidir())) # ideal - self.load = self.Block(DummyPassive()) # avoid ERC - self.connect(self.res.b, self.load.io) + def __init__(self) -> None: + super().__init__() + self.io = self.Port(DigitalBidir.empty(), [Input]) + + @override + def contents(self) -> None: + super().contents() + self.res = self.Block(Resistor(resistance=10 * kOhm(tol=0.05))) # recommended by ST + self.connect(self.io, self.res.a.adapt_to(DigitalBidir())) # ideal + self.load = self.Block(DummyPassive()) # avoid ERC + self.connect(self.res.b, self.load.io) class StTscReference(Block): - """Reference capacitor for STM micros' TSC peripheral.""" - def __init__(self) -> None: - super().__init__() - self.gnd = self.Port(Ground.empty(), [Common]) - self.io = self.Port(DigitalBidir.empty(), [Input]) + """Reference capacitor for STM micros' TSC peripheral.""" + + def __init__(self) -> None: + super().__init__() + self.gnd = self.Port(Ground.empty(), [Common]) + self.io = self.Port(DigitalBidir.empty(), [Input]) - @override - def contents(self) -> None: - super().contents() - self.cap = self.Block(Capacitor(10*nFarad(tol=0.2), voltage=self.io.link().voltage)) - self.connect(self.cap.pos.adapt_to(DigitalBidir()), self.io) - self.connect(self.cap.neg.adapt_to(Ground()), self.gnd) + @override + def contents(self) -> None: + super().contents() + self.cap = self.Block(Capacitor(10 * nFarad(tol=0.2), voltage=self.io.link().voltage)) + self.connect(self.cap.pos.adapt_to(DigitalBidir()), self.io) + self.connect(self.cap.neg.adapt_to(Ground()), self.gnd) class UsbKey(JlcBoardTop): - """USB dongle with the PCB as the USB-A contact surface and a microcontroller on the opposite side. - Similar circuitry and same pinning as the Solokeys Somu: https://github.com/solokeys/solo-hw/tree/master/solo - """ - @override - def contents(self) -> None: - super().contents() + """USB dongle with the PCB as the USB-A contact surface and a microcontroller on the opposite side. + Similar circuitry and same pinning as the Solokeys Somu: https://github.com/solokeys/solo-hw/tree/master/solo + """ + + @override + def contents(self) -> None: + super().contents() - self.usb = self.Block(UsbAPlugPads()) + self.usb = self.Block(UsbAPlugPads()) - self.gnd = self.connect(self.usb.gnd) + self.gnd = self.connect(self.usb.gnd) - # POWER - with self.implicit_connect( + # POWER + with self.implicit_connect( ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.reg_3v3, ), _ = self.chain( - self.usb.pwr, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3V3 DOMAIN - with self.implicit_connect( + ) as imp: + (self.reg_3v3,), _ = self.chain( + self.usb.pwr, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3V3 DOMAIN + with self.implicit_connect( ImplicitConnect(self.v3v3, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.mcu = imp.Block(IoController()) - - self.connect(self.mcu.usb.request('usb'), self.usb.usb) - - (self.rgb, ), _ = self.chain(imp.Block(IndicatorSinkRgbLed()), self.mcu.gpio.request_vector('rgb')) - - (self.ts1, ), _ = self.chain(self.mcu.gpio.request('touch1'), imp.Block(StTscSenseChannel())) - (self.ts2, ), _ = self.chain(self.mcu.gpio.request('touch2'), imp.Block(StTscSenseChannel())) - (self.tss, ), _ = self.chain(self.mcu.gpio.request('ref'), imp.Block(StTscReference())) - - self.connect(self.mcu.gpio.request('b1_gnd'), self.mcu.gpio.request('c15_gnd'), self.usb.gnd.as_digital_source()) - - @override - def multipack(self) -> None: - self.packed_mcu_vdda_cap = self.PackedBlock(CombinedCapacitor()) - self.pack(self.packed_mcu_vdda_cap.elements.request('0'), ['mcu', 'vdda_cap0', 'cap']) - self.pack(self.packed_mcu_vdda_cap.elements.request('1'), ['mcu', 'vdda_cap1', 'cap']) - self.pack(self.packed_mcu_vdda_cap.elements.request('2'), ['mcu', 'vdd_cap[0]', 'cap']) - - self.packed_mcu_vdd1_cap = self.PackedBlock(CombinedCapacitor(extend_upper=True)) - self.pack(self.packed_mcu_vdd1_cap.elements.request('0'), ['reg_3v3', 'out_cap', 'cap']) - self.pack(self.packed_mcu_vdd1_cap.elements.request('1'), ['mcu', 'vdd_cap_bulk', 'cap']) - self.pack(self.packed_mcu_vdd1_cap.elements.request('2'), ['mcu', 'vdd_cap[1]', 'cap']) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Stm32l432k), - (['reg_3v3'], Lp5907), - ], - class_refinements=[ - (RgbLedCommonAnode, Smt0404RgbLed), - (SwdCortexTargetHeader, SwdCortexTargetTagConnect), - (TagConnect, TagConnectNonLegged), - ], - instance_values=[ - (['mcu', 'pin_assigns'], [ - 'touch1=PB4', - 'touch2=PB5', - 'ref=PB6', - - 'rgb_red=PA1', - 'rgb_green=PA2', - 'rgb_blue=PA3', - - # these are hard-tied in the reference and used to help routing here - 'b1_gnd=PB1', - 'c15_gnd=PC15', - ]), - (['mcu', 'swd_connect_reset'], False), - (['packed_mcu_vdd1_cap', 'cap', 'capacitance_minimum_size'], False), - ], - class_values=[ - (SelectorArea, ['footprint_area'], Range.from_lower(1.5)), # at least 0402 - (Lp5907, ['ic', 'footprint_spec'], 'Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm'), - ] - ) + ) as imp: + self.mcu = imp.Block(IoController()) + + self.connect(self.mcu.usb.request("usb"), self.usb.usb) + + (self.rgb,), _ = self.chain(imp.Block(IndicatorSinkRgbLed()), self.mcu.gpio.request_vector("rgb")) + + (self.ts1,), _ = self.chain(self.mcu.gpio.request("touch1"), imp.Block(StTscSenseChannel())) + (self.ts2,), _ = self.chain(self.mcu.gpio.request("touch2"), imp.Block(StTscSenseChannel())) + (self.tss,), _ = self.chain(self.mcu.gpio.request("ref"), imp.Block(StTscReference())) + + self.connect( + self.mcu.gpio.request("b1_gnd"), self.mcu.gpio.request("c15_gnd"), self.usb.gnd.as_digital_source() + ) + + @override + def multipack(self) -> None: + self.packed_mcu_vdda_cap = self.PackedBlock(CombinedCapacitor()) + self.pack(self.packed_mcu_vdda_cap.elements.request("0"), ["mcu", "vdda_cap0", "cap"]) + self.pack(self.packed_mcu_vdda_cap.elements.request("1"), ["mcu", "vdda_cap1", "cap"]) + self.pack(self.packed_mcu_vdda_cap.elements.request("2"), ["mcu", "vdd_cap[0]", "cap"]) + + self.packed_mcu_vdd1_cap = self.PackedBlock(CombinedCapacitor(extend_upper=True)) + self.pack(self.packed_mcu_vdd1_cap.elements.request("0"), ["reg_3v3", "out_cap", "cap"]) + self.pack(self.packed_mcu_vdd1_cap.elements.request("1"), ["mcu", "vdd_cap_bulk", "cap"]) + self.pack(self.packed_mcu_vdd1_cap.elements.request("2"), ["mcu", "vdd_cap[1]", "cap"]) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Stm32l432k), + (["reg_3v3"], Lp5907), + ], + class_refinements=[ + (RgbLedCommonAnode, Smt0404RgbLed), + (SwdCortexTargetHeader, SwdCortexTargetTagConnect), + (TagConnect, TagConnectNonLegged), + ], + instance_values=[ + ( + ["mcu", "pin_assigns"], + [ + "touch1=PB4", + "touch2=PB5", + "ref=PB6", + "rgb_red=PA1", + "rgb_green=PA2", + "rgb_blue=PA3", + # these are hard-tied in the reference and used to help routing here + "b1_gnd=PB1", + "c15_gnd=PC15", + ], + ), + (["mcu", "swd_connect_reset"], False), + (["packed_mcu_vdd1_cap", "cap", "capacitance_minimum_size"], False), + ], + class_values=[ + (SelectorArea, ["footprint_area"], Range.from_lower(1.5)), # at least 0402 + (Lp5907, ["ic", "footprint_spec"], "Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm"), + ], + ) class UsbKeyTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(UsbKey) + def test_design(self) -> None: + compile_board_inplace(UsbKey) diff --git a/examples/test_usb_source_measure.py b/examples/test_usb_source_measure.py index d066ecc92..ce1085136 100644 --- a/examples/test_usb_source_measure.py +++ b/examples/test_usb_source_measure.py @@ -11,1014 +11,1109 @@ class SourceMeasureDutConnector(Connector): - def __init__(self) -> None: - super().__init__() - self.conn = self.Block(PinHeader254Horizontal(3)) - self.gnd = self.Export(self.conn.pins.request('1').adapt_to(Ground()), [Common]) - self.io0 = self.Export(self.conn.pins.request('2').adapt_to(DigitalBidir())) - self.io1 = self.Export(self.conn.pins.request('3').adapt_to(DigitalBidir())) + def __init__(self) -> None: + super().__init__() + self.conn = self.Block(PinHeader254Horizontal(3)) + self.gnd = self.Export(self.conn.pins.request("1").adapt_to(Ground()), [Common]) + self.io0 = self.Export(self.conn.pins.request("2").adapt_to(DigitalBidir())) + self.io1 = self.Export(self.conn.pins.request("3").adapt_to(DigitalBidir())) class SourceMeasureFan(Connector): - def __init__(self) -> None: - super().__init__() - self.conn = self.Block(JstPhKVertical(2)) - self.gnd = self.Export(self.conn.pins.request('1').adapt_to(Ground()), [Common]) - self.pwr = self.Export(self.conn.pins.request('2').adapt_to(VoltageSink( - voltage_limits=5*Volt(tol=0.1), - current_draw=200*mAmp(tol=0) - )), [Power]) + def __init__(self) -> None: + super().__init__() + self.conn = self.Block(JstPhKVertical(2)) + self.gnd = self.Export(self.conn.pins.request("1").adapt_to(Ground()), [Common]) + self.pwr = self.Export( + self.conn.pins.request("2").adapt_to( + VoltageSink(voltage_limits=5 * Volt(tol=0.1), current_draw=200 * mAmp(tol=0)) + ), + [Power], + ) class SourceMeasureRangingCell(Interface, KiCadSchematicBlock): - def __init__(self, resistance: RangeLike): - super().__init__() - self.resistance = self.ArgParameter(resistance) - self.actual_resistance = self.Parameter(RangeExpr()) - - self.gnd = self.Port(Ground.empty(), [Common]) - self.pwr = self.Port(VoltageSink.empty(), [Power]) - - self.pwr_in = self.Port(VoltageSink.empty()) - self.pwr_out = self.Port(VoltageSource.empty()) - - self.control = self.Port(DigitalSink.empty()) - self.sense_in = self.Port(AnalogSource.empty()) - self.sense_out = self.Port(AnalogSource.empty()) - - @override - def contents(self) -> None: - super().contents() - self.import_kicad(self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), - locals={ - 'clamp': { - 'clamp_current': (5, 25)*mAmp, - 'clamp_target': (0, float('inf'))*Volt, - 'zero_out': True - }, - 'resistance': self.resistance - }) - self.sense_sw: AnalogMuxer # schematic-defined - self.connect(self.control, self.sense_sw.control.request()) - self.isense: CurrentSenseResistor - self.assign(self.actual_resistance, self.isense.actual_resistance) + def __init__(self, resistance: RangeLike): + super().__init__() + self.resistance = self.ArgParameter(resistance) + self.actual_resistance = self.Parameter(RangeExpr()) + + self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + + self.pwr_in = self.Port(VoltageSink.empty()) + self.pwr_out = self.Port(VoltageSource.empty()) + + self.control = self.Port(DigitalSink.empty()) + self.sense_in = self.Port(AnalogSource.empty()) + self.sense_out = self.Port(AnalogSource.empty()) + + @override + def contents(self) -> None: + super().contents() + self.import_kicad( + self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), + locals={ + "clamp": {"clamp_current": (5, 25) * mAmp, "clamp_target": (0, float("inf")) * Volt, "zero_out": True}, + "resistance": self.resistance, + }, + ) + self.sense_sw: AnalogMuxer # schematic-defined + self.connect(self.control, self.sense_sw.control.request()) + self.isense: CurrentSenseResistor + self.assign(self.actual_resistance, self.isense.actual_resistance) class RangingCurrentSenseResistor(Interface, KiCadImportableBlock, GeneratorBlock): - """Generates an array of current-sense resistors with one side muxed and the other end an array. - The resistors are tied common on the com side, and have a solid-state relay for the power path - on the input side. Each resistor has an analog switch on the input sense side. - - The control line is one bit for each range (range connectivity is independent). - Multiple ranges can be connected simultaneously, this allows make-before-break connectivity.""" - @override - def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: - assert symbol_name == 'edg_importable:CurrentSenseResistorMux' - return { - 'control': self.control, 'sw': self.pwr_in, 'com': self.pwr_out, - 'sen_sw': self.sense_in, 'sen_com': self.sense_out, - 'V+': self.pwr, 'V-': self.gnd - } - - def __init__(self, resistances: ArrayRangeLike, currents: ArrayRangeLike): - super().__init__() - - self.gnd = self.Port(Ground.empty(), [Common]) - self.pwr = self.Port(VoltageSink.empty(), [Power]) - - self.pwr_in = self.Port(VoltageSink.empty()) - self.pwr_out = self.Port(VoltageSource.empty()) - - self.control = self.Port(Vector(DigitalSink.empty())) - self.sense_in = self.Port(AnalogSource.empty()) - self.sense_out = self.Port(AnalogSource.empty()) - - self.resistances = self.ArgParameter(resistances) - self.currents = self.ArgParameter(currents) - self.generator_param(self.resistances, self.currents) - - self.out_range = self.Parameter(RangeExpr()) - - @override - def generate(self) -> None: - super().generate() - self.ranges = ElementDict[SourceMeasureRangingCell]() - - self.forced = ElementDict[ForcedVoltageCurrentDraw]() - self.pwr_out_merge = self.Block(MergedVoltageSource()) - self.connect(self.pwr_out_merge.pwr_out, self.pwr_out) - self.sense_in_merge = self.Block(MergedAnalogSource()) - self.connect(self.sense_in_merge.output, self.sense_in) - self.sense_out_merge = self.Block(MergedAnalogSource()) - self.connect(self.sense_out_merge.output, self.sense_out) - - out_range = RangeExpr._to_expr_type(RangeExpr.EMPTY) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ImplicitConnect(self.pwr, [Power]) - ) as imp: - for i, (resistance, current) in enumerate(zip(self.get(self.resistances), self.get(self.currents))): - range = self.ranges[i] = imp.Block(SourceMeasureRangingCell(resistance)) - self.connect(self.pwr_in, range.pwr_in) - forced = self.forced[i] = self.Block(ForcedVoltageCurrentDraw(current)) - self.connect(range.pwr_out, forced.pwr_in) - self.connect(self.pwr_out_merge.pwr_ins.request(str(i)), forced.pwr_out) + """Generates an array of current-sense resistors with one side muxed and the other end an array. + The resistors are tied common on the com side, and have a solid-state relay for the power path + on the input side. Each resistor has an analog switch on the input sense side. + + The control line is one bit for each range (range connectivity is independent). + Multiple ranges can be connected simultaneously, this allows make-before-break connectivity.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]: + assert symbol_name == "edg_importable:CurrentSenseResistorMux" + return { + "control": self.control, + "sw": self.pwr_in, + "com": self.pwr_out, + "sen_sw": self.sense_in, + "sen_com": self.sense_out, + "V+": self.pwr, + "V-": self.gnd, + } + + def __init__(self, resistances: ArrayRangeLike, currents: ArrayRangeLike): + super().__init__() + + self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr = self.Port(VoltageSink.empty(), [Power]) + + self.pwr_in = self.Port(VoltageSink.empty()) + self.pwr_out = self.Port(VoltageSource.empty()) + + self.control = self.Port(Vector(DigitalSink.empty())) + self.sense_in = self.Port(AnalogSource.empty()) + self.sense_out = self.Port(AnalogSource.empty()) + + self.resistances = self.ArgParameter(resistances) + self.currents = self.ArgParameter(currents) + self.generator_param(self.resistances, self.currents) + + self.out_range = self.Parameter(RangeExpr()) + + @override + def generate(self) -> None: + super().generate() + self.ranges = ElementDict[SourceMeasureRangingCell]() + + self.forced = ElementDict[ForcedVoltageCurrentDraw]() + self.pwr_out_merge = self.Block(MergedVoltageSource()) + self.connect(self.pwr_out_merge.pwr_out, self.pwr_out) + self.sense_in_merge = self.Block(MergedAnalogSource()) + self.connect(self.sense_in_merge.output, self.sense_in) + self.sense_out_merge = self.Block(MergedAnalogSource()) + self.connect(self.sense_out_merge.output, self.sense_out) + + out_range = RangeExpr._to_expr_type(RangeExpr.EMPTY) - self.connect(range.control, self.control.append_elt(DigitalSink.empty(), str(i))) - self.connect(self.sense_in_merge.inputs.request(str(i)), range.sense_in) - self.connect(self.sense_out_merge.inputs.request(str(i)), range.sense_out) + with self.implicit_connect(ImplicitConnect(self.gnd, [Common]), ImplicitConnect(self.pwr, [Power])) as imp: + for i, (resistance, current) in enumerate(zip(self.get(self.resistances), self.get(self.currents))): + range = self.ranges[i] = imp.Block(SourceMeasureRangingCell(resistance)) + self.connect(self.pwr_in, range.pwr_in) + forced = self.forced[i] = self.Block(ForcedVoltageCurrentDraw(current)) + self.connect(range.pwr_out, forced.pwr_in) + self.connect(self.pwr_out_merge.pwr_ins.request(str(i)), forced.pwr_out) - out_range = out_range.hull(current * range.actual_resistance) + self.connect(range.control, self.control.append_elt(DigitalSink.empty(), str(i))) + self.connect(self.sense_in_merge.inputs.request(str(i)), range.sense_in) + self.connect(self.sense_out_merge.inputs.request(str(i)), range.sense_out) - self.assign(self.out_range, out_range) + out_range = out_range.hull(current * range.actual_resistance) + + self.assign(self.out_range, out_range) class EmitterFollower(InternalSubcircuit, KiCadSchematicBlock, KiCadImportableBlock, Block): - """Emitter follower circuit. - """ - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name == 'edg_importable:Follower' # this requires an schematic-modified symbol - return { - '1': self.control, '3': self.out, 'V+': self.pwr, 'V-': self.gnd, - 'HG': self.high_gate_ctl, 'LG': self.low_gate_ctl, - 'VG+': self.pwr_gate_pos, 'VG-': self.pwr_gate_neg - } - - def __init__(self, current: RangeLike, rds_on: RangeLike, gate_clamp_voltage: RangeLike): - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - self.pwr_gate_pos = self.Port(VoltageSink.empty()) - self.pwr_gate_neg = self.Port(Ground.empty()) - self.out = self.Port(VoltageSource.empty()) - - self.control = self.Port(AnalogSink.empty()) - self.high_gate_ctl = self.Port(DigitalSink.empty()) - self.low_gate_ctl = self.Port(DigitalSink.empty()) - - self.current = self.ArgParameter(current) - self.rds_on = self.ArgParameter(rds_on) - self.gate_clamp_voltage = self.ArgParameter(gate_clamp_voltage) - - @override - def contents(self) -> None: - super().contents() - - zener_model = ZenerDiode(self.gate_clamp_voltage) - self.clamp1 = self.Block(zener_model) - self.clamp2 = self.Block(zener_model) - - gate_voltage = (-self.clamp1.actual_zener_voltage).hull(self.clamp2.actual_zener_voltage) - self.high_fet = self.Block(Fet.NFet( - drain_voltage=self.pwr.link().voltage, - drain_current=self.current, - gate_voltage=gate_voltage, - rds_on=self.rds_on, - power=self.pwr.link().voltage * self.current)) - self.low_fet = self.Block(Fet.PFet( - drain_voltage=self.pwr.link().voltage, - drain_current=self.current, - gate_voltage=gate_voltage, - rds_on=self.rds_on, - power=self.pwr.link().voltage * self.current)) - resistance = 3.0*kOhm(tol=0.05) # 3 x 1k in series - max_clamp_voltage = VoltageLink._supply_voltage_range(self.pwr_gate_neg, self.pwr_gate_pos).upper() - self.gate_clamp_voltage.lower() - self.res = self.Block(Resistor( - resistance=resistance, - power=(0, max_clamp_voltage * max_clamp_voltage / resistance.lower()), - voltage=(0, max_clamp_voltage) - )) - - self.import_kicad(self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'low_fet.D': Ground(), - 'high_fet.D': VoltageSink( - current_draw=self.current, - voltage_limits=self.high_fet.actual_drain_voltage_rating.intersect( - self.low_fet.actual_drain_voltage_rating) - ), - 'out': VoltageSource( - voltage_out=self.pwr.link().voltage, - current_limits=self.high_fet.actual_drain_current_rating.intersect(self.low_fet.actual_drain_current_rating) - ), - 'control': AnalogSink(), - - # TODO FIXME - 'res.2': AnalogSource(), - 'clamp1.A': AnalogSink(), - 'low_res.1': AnalogSink(), - 'low_fet.G': AnalogSink(), - 'high_res.1': AnalogSink(), - 'high_fet.G': AnalogSink(), - }) - - self.high_gate: AnalogMuxer # defined in schematic - self.connect(self.high_gate_ctl, self.high_gate.control.request()) - self.low_gate: AnalogMuxer - self.connect(self.low_gate_ctl, self.low_gate.control.request()) - self.connect(self.gnd, self.high_gate.control_gnd, self.low_gate.control_gnd) + """Emitter follower circuit.""" + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name == "edg_importable:Follower" # this requires an schematic-modified symbol + return { + "1": self.control, + "3": self.out, + "V+": self.pwr, + "V-": self.gnd, + "HG": self.high_gate_ctl, + "LG": self.low_gate_ctl, + "VG+": self.pwr_gate_pos, + "VG-": self.pwr_gate_neg, + } + + def __init__(self, current: RangeLike, rds_on: RangeLike, gate_clamp_voltage: RangeLike): + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + self.pwr_gate_pos = self.Port(VoltageSink.empty()) + self.pwr_gate_neg = self.Port(Ground.empty()) + self.out = self.Port(VoltageSource.empty()) + + self.control = self.Port(AnalogSink.empty()) + self.high_gate_ctl = self.Port(DigitalSink.empty()) + self.low_gate_ctl = self.Port(DigitalSink.empty()) + + self.current = self.ArgParameter(current) + self.rds_on = self.ArgParameter(rds_on) + self.gate_clamp_voltage = self.ArgParameter(gate_clamp_voltage) + + @override + def contents(self) -> None: + super().contents() + + zener_model = ZenerDiode(self.gate_clamp_voltage) + self.clamp1 = self.Block(zener_model) + self.clamp2 = self.Block(zener_model) + + gate_voltage = (-self.clamp1.actual_zener_voltage).hull(self.clamp2.actual_zener_voltage) + self.high_fet = self.Block( + Fet.NFet( + drain_voltage=self.pwr.link().voltage, + drain_current=self.current, + gate_voltage=gate_voltage, + rds_on=self.rds_on, + power=self.pwr.link().voltage * self.current, + ) + ) + self.low_fet = self.Block( + Fet.PFet( + drain_voltage=self.pwr.link().voltage, + drain_current=self.current, + gate_voltage=gate_voltage, + rds_on=self.rds_on, + power=self.pwr.link().voltage * self.current, + ) + ) + resistance = 3.0 * kOhm(tol=0.05) # 3 x 1k in series + max_clamp_voltage = ( + VoltageLink._supply_voltage_range(self.pwr_gate_neg, self.pwr_gate_pos).upper() + - self.gate_clamp_voltage.lower() + ) + self.res = self.Block( + Resistor( + resistance=resistance, + power=(0, max_clamp_voltage * max_clamp_voltage / resistance.lower()), + voltage=(0, max_clamp_voltage), + ) + ) + + self.import_kicad( + self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "low_fet.D": Ground(), + "high_fet.D": VoltageSink( + current_draw=self.current, + voltage_limits=self.high_fet.actual_drain_voltage_rating.intersect( + self.low_fet.actual_drain_voltage_rating + ), + ), + "out": VoltageSource( + voltage_out=self.pwr.link().voltage, + current_limits=self.high_fet.actual_drain_current_rating.intersect( + self.low_fet.actual_drain_current_rating + ), + ), + "control": AnalogSink(), + # TODO FIXME + "res.2": AnalogSource(), + "clamp1.A": AnalogSink(), + "low_res.1": AnalogSink(), + "low_fet.G": AnalogSink(), + "high_res.1": AnalogSink(), + "high_fet.G": AnalogSink(), + }, + ) + + self.high_gate: AnalogMuxer # defined in schematic + self.connect(self.high_gate_ctl, self.high_gate.control.request()) + self.low_gate: AnalogMuxer + self.connect(self.low_gate_ctl, self.low_gate.control.request()) + self.connect(self.gnd, self.high_gate.control_gnd, self.low_gate.control_gnd) class GatedSummingAmplifier(InternalSubcircuit, KiCadSchematicBlock, KiCadImportableBlock, GeneratorBlock): - """A noninverting summing amplifier with an optional diode gate (enforcing drive direction) and inline resistance - (to allow its output to be overridden by a stronger driver). - - Used as the error amplifier in SMU analog control block, the target is set with inverted polarity - (around the integrator reference). When the measured signal is at the target, the output (sum) - is the integrator reference, producing zero error. Otherwise, the error signal is proportional to the deviation. - - The sense_out line is upstream of this element and can be used to determine if a current limit amplifier is active. - - TODO: diode parameter should be an enum. Current values: '' (no diode), 'sink', 'source' (sinks or sources current) - """ - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name in ('Simulation_SPICE:OPAMP', 'edg_importable:Opamp') - return {'M': self.actual, 'T': self.target, 'F': self.target_fine, '3': self.output, 'S': self.sense_out, - 'V+': self.pwr, 'V-': self.gnd} - - def __init__(self, input_resistance: RangeLike = 0*Ohm(tol=0), *, - dir: StringLike = '', res: RangeLike = 0*Ohm(tol=0), fine_scale: FloatLike = 0, - series: IntLike = 24, tolerance: FloatLike = 0.01): - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.gnd = self.Port(Ground.empty(), [Common]) - - self.target = self.Port(AnalogSink.empty()) - self.target_fine = self.Port(AnalogSink.empty(), optional=True) - self.actual = self.Port(AnalogSink.empty()) - self.output = self.Port(AnalogSource.empty()) - self.sense_out = self.Port(AnalogSource.empty(), optional=True) - - self.input_resistance = self.ArgParameter(input_resistance) - self.dir = self.ArgParameter(dir) - self.res = self.ArgParameter(res) # output side - self.fine_scale = self.ArgParameter(fine_scale) - self.series = self.ArgParameter(series) - self.tolerance = self.ArgParameter(tolerance) - self.generator_param(self.input_resistance, self.res, self.dir, self.series, self.tolerance, - self.target_fine.is_connected(), self.sense_out.is_connected(), self.fine_scale) - - @override - def generate(self) -> None: - super().generate() - - # The 1/4 factor is a way to specify the series resistance of the divider assuming both resistors are equal, - # since the DividerValues util only takes the parallel resistance - calculator = ESeriesRatioUtil(ESeriesUtil.SERIES[self.get(self.series)], self.get(self.tolerance), DividerValues) - top_resistance, bottom_resistance = calculator.find(DividerValues(Range.from_tolerance(0.5, self.get(self.tolerance)), - self.get(self.input_resistance) / 4)) - - self.amp = self.Block(Opamp()) - self.rtop = self.Block(Resistor(resistance=Range.from_tolerance(top_resistance, self.get(self.tolerance)))) - self.rbot = self.Block(Resistor(resistance=Range.from_tolerance(bottom_resistance, self.get(self.tolerance)))) - - output_impedance = self.amp.out.link().source_impedance - dir = self.get(self.dir) - if dir: - self.diode = self.Block(Diode( # TODO should be encoded as a voltage difference? - reverse_voltage=self.amp.out.voltage_out, - current=RangeExpr.ZERO, # an approximation, current rating not significant here - voltage_drop=(0, 0.8)*Volt # arbitrary low threshold - )) - amp_out_model = AnalogSink( - impedance=self.output.link().sink_impedance - ) - if dir == 'source': - self.connect(self.amp.out, self.diode.anode.adapt_to(amp_out_model)) - amp_out_node = self.diode.cathode - elif dir == 'sink': - self.connect(self.amp.out, self.diode.cathode.adapt_to(amp_out_model)) - amp_out_node = self.diode.anode - else: - raise ValueError(f"invalid dir '{dir}', expected '', 'source', or 'sink'") - - if self.get(self.res) != Range.exact(0): # if resistor requested - assert not dir, "diode + output resistance not supported" - self.rout = self.Block(Resistor(resistance=self.res)) - self.connect(self.amp.out, self.rout.a.adapt_to(AnalogSink( - impedance=self.rout.actual_resistance + self.output.link().sink_impedance - ))) - amp_out_node = self.rout.b - output_impedance += self.rout.actual_resistance - - self.connect(amp_out_node.adapt_to(AnalogSource( - impedance=output_impedance - )), self.output) - - self.import_kicad(self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'target': AnalogSink( - impedance=self.rtop.actual_resistance + self.rbot.actual_resistance # assumed dominates fine resistance - ), - 'actual': AnalogSink( - impedance=self.rtop.actual_resistance + self.rbot.actual_resistance - ), - 'rtop.2': AnalogSource( - voltage_out=self.target.link().voltage.hull(self.actual.link().voltage), - signal_out=self.target.link().voltage.hull(self.actual.link().voltage), - impedance=1 / (1 / self.rtop.actual_resistance + 1 / self.rbot.actual_resistance) - ), - 'rbot.2': AnalogSink(), # ideal, rtop.2 contains the parameter model - }) - - if self.get(self.target_fine.is_connected()): - assert self.get(self.fine_scale) != 0 - self.rfine = self.Block(Resistor(resistance=Range.from_tolerance(top_resistance * self.get(self.fine_scale), - self.get(self.tolerance)))) - self.connect(self.target_fine, self.rfine.a.adapt_to(AnalogSink( - impedance=self.rfine.actual_resistance # assumed non-fine resistance dominates - ))) - self.connect(self.rfine.b.adapt_to(AnalogSink()), self.amp.inp) - - if self.get(self.sense_out.is_connected()): - self.connect(self.amp.out, self.sense_out) + """A noninverting summing amplifier with an optional diode gate (enforcing drive direction) and inline resistance + (to allow its output to be overridden by a stronger driver). + + Used as the error amplifier in SMU analog control block, the target is set with inverted polarity + (around the integrator reference). When the measured signal is at the target, the output (sum) + is the integrator reference, producing zero error. Otherwise, the error signal is proportional to the deviation. + + The sense_out line is upstream of this element and can be used to determine if a current limit amplifier is active. + + TODO: diode parameter should be an enum. Current values: '' (no diode), 'sink', 'source' (sinks or sources current) + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name in ("Simulation_SPICE:OPAMP", "edg_importable:Opamp") + return { + "M": self.actual, + "T": self.target, + "F": self.target_fine, + "3": self.output, + "S": self.sense_out, + "V+": self.pwr, + "V-": self.gnd, + } + + def __init__( + self, + input_resistance: RangeLike = 0 * Ohm(tol=0), + *, + dir: StringLike = "", + res: RangeLike = 0 * Ohm(tol=0), + fine_scale: FloatLike = 0, + series: IntLike = 24, + tolerance: FloatLike = 0.01, + ): + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.gnd = self.Port(Ground.empty(), [Common]) + + self.target = self.Port(AnalogSink.empty()) + self.target_fine = self.Port(AnalogSink.empty(), optional=True) + self.actual = self.Port(AnalogSink.empty()) + self.output = self.Port(AnalogSource.empty()) + self.sense_out = self.Port(AnalogSource.empty(), optional=True) + + self.input_resistance = self.ArgParameter(input_resistance) + self.dir = self.ArgParameter(dir) + self.res = self.ArgParameter(res) # output side + self.fine_scale = self.ArgParameter(fine_scale) + self.series = self.ArgParameter(series) + self.tolerance = self.ArgParameter(tolerance) + self.generator_param( + self.input_resistance, + self.res, + self.dir, + self.series, + self.tolerance, + self.target_fine.is_connected(), + self.sense_out.is_connected(), + self.fine_scale, + ) + + @override + def generate(self) -> None: + super().generate() + + # The 1/4 factor is a way to specify the series resistance of the divider assuming both resistors are equal, + # since the DividerValues util only takes the parallel resistance + calculator = ESeriesRatioUtil( + ESeriesUtil.SERIES[self.get(self.series)], self.get(self.tolerance), DividerValues + ) + top_resistance, bottom_resistance = calculator.find( + DividerValues(Range.from_tolerance(0.5, self.get(self.tolerance)), self.get(self.input_resistance) / 4) + ) + + self.amp = self.Block(Opamp()) + self.rtop = self.Block(Resistor(resistance=Range.from_tolerance(top_resistance, self.get(self.tolerance)))) + self.rbot = self.Block(Resistor(resistance=Range.from_tolerance(bottom_resistance, self.get(self.tolerance)))) + + output_impedance = self.amp.out.link().source_impedance + dir = self.get(self.dir) + if dir: + self.diode = self.Block( + Diode( # TODO should be encoded as a voltage difference? + reverse_voltage=self.amp.out.voltage_out, + current=RangeExpr.ZERO, # an approximation, current rating not significant here + voltage_drop=(0, 0.8) * Volt, # arbitrary low threshold + ) + ) + amp_out_model = AnalogSink(impedance=self.output.link().sink_impedance) + if dir == "source": + self.connect(self.amp.out, self.diode.anode.adapt_to(amp_out_model)) + amp_out_node = self.diode.cathode + elif dir == "sink": + self.connect(self.amp.out, self.diode.cathode.adapt_to(amp_out_model)) + amp_out_node = self.diode.anode + else: + raise ValueError(f"invalid dir '{dir}', expected '', 'source', or 'sink'") + + if self.get(self.res) != Range.exact(0): # if resistor requested + assert not dir, "diode + output resistance not supported" + self.rout = self.Block(Resistor(resistance=self.res)) + self.connect( + self.amp.out, + self.rout.a.adapt_to( + AnalogSink(impedance=self.rout.actual_resistance + self.output.link().sink_impedance) + ), + ) + amp_out_node = self.rout.b + output_impedance += self.rout.actual_resistance + + self.connect(amp_out_node.adapt_to(AnalogSource(impedance=output_impedance)), self.output) + + self.import_kicad( + self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "target": AnalogSink( + impedance=self.rtop.actual_resistance + + self.rbot.actual_resistance # assumed dominates fine resistance + ), + "actual": AnalogSink(impedance=self.rtop.actual_resistance + self.rbot.actual_resistance), + "rtop.2": AnalogSource( + voltage_out=self.target.link().voltage.hull(self.actual.link().voltage), + signal_out=self.target.link().voltage.hull(self.actual.link().voltage), + impedance=1 / (1 / self.rtop.actual_resistance + 1 / self.rbot.actual_resistance), + ), + "rbot.2": AnalogSink(), # ideal, rtop.2 contains the parameter model + }, + ) + + if self.get(self.target_fine.is_connected()): + assert self.get(self.fine_scale) != 0 + self.rfine = self.Block( + Resistor( + resistance=Range.from_tolerance( + top_resistance * self.get(self.fine_scale), self.get(self.tolerance) + ) + ) + ) + self.connect( + self.target_fine, + self.rfine.a.adapt_to( + AnalogSink(impedance=self.rfine.actual_resistance) # assumed non-fine resistance dominates + ), + ) + self.connect(self.rfine.b.adapt_to(AnalogSink()), self.amp.inp) + + if self.get(self.sense_out.is_connected()): + self.connect(self.amp.out, self.sense_out) class JfetCurrentClamp(InternalSubcircuit, KiCadSchematicBlock, KiCadImportableBlock, Block): - """JET-based current clamp, clamps to roughly 10mA while maintaining a relatively low non-clamping - impedance of ~100ohm. Max ~35V limited by JFET Vgs,max. - """ - @override - def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: - assert symbol_name == 'edg_importable:Unk2' - return {'1': self.input, '2': self.output} - - def __init__(self, model_voltage_clamp: RangeLike, model_signal_clamp: RangeLike = RangeExpr.ALL): - super().__init__() - - self.model_voltage_clamp = self.ArgParameter(model_voltage_clamp) - self.model_signal_clamp = self.ArgParameter(model_signal_clamp) - - self.input = self.Port(AnalogSink.empty(), [Power]) - self.output = self.Port(AnalogSource.empty(), [Common]) - - @override - def contents(self) -> None: - super().contents() - - self.import_kicad(self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), - conversions={ - 'input': AnalogSink(current_draw=self.output.link().current_drawn, - impedance=self.output.link().sink_impedance), - 'output': AnalogSource(voltage_out=self.input.link().voltage.intersect( - self.model_voltage_clamp), - signal_out=self.input.link().signal.intersect( - self.model_signal_clamp), - impedance=self.input.link().source_impedance) - }) + """JET-based current clamp, clamps to roughly 10mA while maintaining a relatively low non-clamping + impedance of ~100ohm. Max ~35V limited by JFET Vgs,max. + """ + + @override + def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]: + assert symbol_name == "edg_importable:Unk2" + return {"1": self.input, "2": self.output} + + def __init__(self, model_voltage_clamp: RangeLike, model_signal_clamp: RangeLike = RangeExpr.ALL): + super().__init__() + + self.model_voltage_clamp = self.ArgParameter(model_voltage_clamp) + self.model_signal_clamp = self.ArgParameter(model_signal_clamp) + + self.input = self.Port(AnalogSink.empty(), [Power]) + self.output = self.Port(AnalogSource.empty(), [Common]) + + @override + def contents(self) -> None: + super().contents() + + self.import_kicad( + self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), + conversions={ + "input": AnalogSink( + current_draw=self.output.link().current_drawn, impedance=self.output.link().sink_impedance + ), + "output": AnalogSource( + voltage_out=self.input.link().voltage.intersect(self.model_voltage_clamp), + signal_out=self.input.link().signal.intersect(self.model_signal_clamp), + impedance=self.input.link().source_impedance, + ), + }, + ) class SrLatchInverted(Block): - """Set-reset latch with active-high set, active-high reset, set priority, and low output when set (high when idle). - This uses two NOR gates. - NOR1 handles set with priority, when any input is high, the output goes low. - Latching is done when NOR1 is low, which feeds into NOR2. If reset isn't asserted, both NOR2 inputs are low - and NOR2 output is high, which feeds back into a NOR1 input to keep NOR1 low. - NOR2 handles reset without priority, when the input goes high, its output goes low which clears the latch. - """ - def __init__(self) -> None: - super().__init__() - self.ic = self.Block(Sn74lvc2g02()) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.pwr = self.Export(self.ic.pwr, [Power]) - - self.set = self.Export(self.ic.in1a) # any in1 - self.rst = self.Export(self.ic.in2a) # any in2 - self.out = self.Export(self.ic.out1) - - @override - def contents(self) -> None: - super().contents() - self.connect(self.ic.out1, self.ic.in2b) - self.connect(self.ic.out2, self.ic.in1b) + """Set-reset latch with active-high set, active-high reset, set priority, and low output when set (high when idle). + This uses two NOR gates. + NOR1 handles set with priority, when any input is high, the output goes low. + Latching is done when NOR1 is low, which feeds into NOR2. If reset isn't asserted, both NOR2 inputs are low + and NOR2 output is high, which feeds back into a NOR1 input to keep NOR1 low. + NOR2 handles reset without priority, when the input goes high, its output goes low which clears the latch. + """ + + def __init__(self) -> None: + super().__init__() + self.ic = self.Block(Sn74lvc2g02()) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.pwr = self.Export(self.ic.pwr, [Power]) + + self.set = self.Export(self.ic.in1a) # any in1 + self.rst = self.Export(self.ic.in2a) # any in2 + self.out = self.Export(self.ic.out1) + + @override + def contents(self) -> None: + super().contents() + self.connect(self.ic.out1, self.ic.in2b) + self.connect(self.ic.out2, self.ic.in1b) class SourceMeasureControl(InternalSubcircuit, KiCadSchematicBlock, Block): - """Analog feedback circuit for the source-measure unit - """ - def __init__(self, current: RangeLike, rds_on: RangeLike): - super().__init__() - - self.pwr = self.Port(VoltageSink.empty(), [Power]) - self.pwr_logic = self.Port(VoltageSink.empty()) - self.gnd = self.Port(Ground.empty(), [Common]) - self.ref_center = self.Port(AnalogSink.empty()) - - self.pwr_gate_pos = self.Port(VoltageSink.empty()) - self.pwr_gate_neg = self.Port(Ground.empty()) - - self.control_voltage = self.Port(AnalogSink.empty()) - self.control_voltage_fine = self.Port(AnalogSink.empty()) - self.control_current_source = self.Port(AnalogSink.empty()) - self.control_current_sink = self.Port(AnalogSink.empty()) - self.high_gate_ctl = self.Port(DigitalSink.empty()) - self.low_gate_ctl = self.Port(DigitalSink.empty()) - self.irange = self.Port(Vector(DigitalSink.empty())) - self.off = self.Port(Vector(DigitalSink.empty())) - self.out = self.Port(VoltageSource.empty()) - - self.measured_voltage = self.Port(AnalogSource.empty()) - self.measured_current = self.Port(AnalogSource.empty()) - self.limit_source = self.Port(DigitalSource.empty()) - self.limit_sink = self.Port(DigitalSource.empty()) - - self.tp_err = self.Port(AnalogSource.empty(), optional=True) - self.tp_int = self.Port(AnalogSource.empty(), optional=True) - - self.current = self.ArgParameter(current) - self.rds_on = self.ArgParameter(rds_on) - - @override - def contents(self) -> None: - super().contents() - - self.import_kicad(self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), - locals={ - 'self': self - }, conversions={ - 'tvs_p.K': VoltageSink(), - 'tvs_n.K': Ground(), - }) + """Analog feedback circuit for the source-measure unit""" + + def __init__(self, current: RangeLike, rds_on: RangeLike): + super().__init__() + + self.pwr = self.Port(VoltageSink.empty(), [Power]) + self.pwr_logic = self.Port(VoltageSink.empty()) + self.gnd = self.Port(Ground.empty(), [Common]) + self.ref_center = self.Port(AnalogSink.empty()) + + self.pwr_gate_pos = self.Port(VoltageSink.empty()) + self.pwr_gate_neg = self.Port(Ground.empty()) + + self.control_voltage = self.Port(AnalogSink.empty()) + self.control_voltage_fine = self.Port(AnalogSink.empty()) + self.control_current_source = self.Port(AnalogSink.empty()) + self.control_current_sink = self.Port(AnalogSink.empty()) + self.high_gate_ctl = self.Port(DigitalSink.empty()) + self.low_gate_ctl = self.Port(DigitalSink.empty()) + self.irange = self.Port(Vector(DigitalSink.empty())) + self.off = self.Port(Vector(DigitalSink.empty())) + self.out = self.Port(VoltageSource.empty()) + + self.measured_voltage = self.Port(AnalogSource.empty()) + self.measured_current = self.Port(AnalogSource.empty()) + self.limit_source = self.Port(DigitalSource.empty()) + self.limit_sink = self.Port(DigitalSource.empty()) + + self.tp_err = self.Port(AnalogSource.empty(), optional=True) + self.tp_int = self.Port(AnalogSource.empty(), optional=True) + + self.current = self.ArgParameter(current) + self.rds_on = self.ArgParameter(rds_on) + + @override + def contents(self) -> None: + super().contents() + + self.import_kicad( + self.file_path("UsbSourceMeasure", f"{self.__class__.__name__}.kicad_sch"), + locals={"self": self}, + conversions={ + "tvs_p.K": VoltageSink(), + "tvs_n.K": Ground(), + }, + ) # JlcPartsRefinements are used in production since the old parts table # list many parts that are no longer basic. # class UsbSourceMeasure(JlcPartsRefinements, JlcBoardTop): class UsbSourceMeasure(JlcBoardTop): - @override - def contents(self) -> None: - super().contents() - - # overall design parameters - OUTPUT_CURRENT_RATING = (0, 3)*Amp - - # USB PD port that supplies power to the load - # USB PD can't actually do 8 A, but this suppresses the error and we can software-limit current draw - self.usb = self.Block(UsbCReceptacle(voltage_out=(5, 20)*Volt, current_limits=(0, 8)*Amp)) - - self.gnd = self.connect(self.usb.gnd) - self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) - - # power supplies - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.vusb_sense = imp.Block(Ina219(10*mOhm(tol=0.01))) - - # input filtering and protection - (self.fuse_vusb, self.filt_vusb, self.prot_vusb), _ = self.chain( - self.usb.pwr, - self.Block(SeriesPowerFuse(trip_current=(7, 8)*Amp)), - self.Block(SeriesPowerFerriteBead()), - imp.Block(ProtectionZenerDiode(voltage=(32, 40)*Volt)), # for parts commonality w/ the Vconv zener - self.vusb_sense.sense_pos - ) - self.vusb = self.connect(self.vusb_sense.sense_neg) - - (self.ramp, self.cap_conv), _ = self.chain( - self.vusb, - imp.Block(RampLimiter( - target_vgs=(3.7, 19)*Volt - )), # avoid excess capacitance on VBus which may cause the PD source to reset - imp.Block(DecouplingCapacitor(47*uFarad(tol=0.25))), - ) - self.vusb_ramp = self.connect(self.ramp.pwr_out) # vusb post-ramp - self.tp_vusb = self.Block(VoltageTestPoint()).connected(self.ramp.pwr_out) - - # logic supplies - (self.reg_v5, self.tp_v5), _ = self.chain( - self.vusb_ramp, # non-critical power supply downstream of ramp limiter - imp.Block(BuckConverter(output_voltage=5*Volt(tol=0.05))), # min set by gate drivers - self.Block(VoltageTestPoint()), - ) - self.v5 = self.connect(self.reg_v5.pwr_out) - - (self.reg_3v3, self.prot_3v3, self.tp_3v3), _ = self.chain( - self.vusb, # upstream of ramp limiter, required for bootstrapping - imp.Block(BuckConverter(output_voltage=3.3*Volt(tol=0.05))), - imp.Block(ProtectionZenerDiode(voltage=(3.6, 4.5)*Volt)), - self.Block(VoltageTestPoint()) - ) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - self.connect(self.vusb_sense.pwr, self.v3v3) - - (self.reg_v12, ), _ = self.chain( - self.v5, - imp.Block(BoostConverter(output_voltage=(12, 13)*Volt)), # limits of the OLED - ) - self.v12 = self.connect(self.reg_v12.pwr_out) - - # output power supplies - self.convin_sense = imp.Block(Ina219(10*mOhm(tol=0.01), addr_lsb=4)) - self.connect(self.convin_sense.pwr, self.v3v3) - self.connect(self.vusb_ramp, self.convin_sense.sense_pos) - self.vconvin = self.connect(self.convin_sense.sense_neg) - (self.conv_inforce, self.conv, self.conv_outforce, self.prot_conv, self.tp_conv), _ = self.chain( - self.vconvin, - imp.Block(ForcedVoltage(20*Volt(tol=0))), # assumed input voltage to target buck-boost ratios - imp.Block(CustomSyncBuckBoostConverterPwm(output_voltage=(15, 30)*Volt, # design for 0.5x - 1.5x conv ratio - frequency=500*kHertz(tol=0), - ripple_ratio=(0.01, 0.9), - input_ripple_limit=(100*(6/7))*mVolt, # fill empty space with caps - output_ripple_limit=(25*(7/8))*mVolt # fill empty space with caps - )), - imp.Block(ForcedVoltage((2, 30)*Volt)), # at least 2v to allow current sensor to work - imp.Block(ProtectionZenerDiode(voltage=(32, 40)*Volt)), # zener shunt in case the boost converter goes crazy - self.Block(VoltageTestPoint()) - ) - self.connect(self.conv.pwr_logic, self.v5) - self.vconv = self.connect(self.conv_outforce.pwr_out) - - # analog supplies - (self.reg_analog, self.tp_analog), _ = self.chain( - self.v5, - imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))), - self.Block(VoltageTestPoint("va")) - ) - self.vanalog = self.connect(self.reg_analog.pwr_out) - - (self.reg_vref, self.tp_vref), _ = self.chain( - self.v5, - imp.Block(VoltageReference(output_voltage=3.3*Volt(tol=0.01))), - self.Block(VoltageTestPoint()) - ) - self.vref = self.connect(self.reg_vref.pwr_out) - - (self.ref_div, self.ref_buf, self.ref_rc), _ = self.chain( - self.vref, - imp.Block(VoltageDivider(output_voltage=1.65*Volt(tol=0.05), impedance=(10, 100)*kOhm)), - imp.Block(OpampFollower()), - # opamp outputs generally not stable under capacitive loading and requires an isolation series resistor - # 4.7 tries to balance low output impedance and some level of isolation - imp.Block(AnalogLowPassRc(4.7*Ohm(tol=0.05), 1*MHertz(tol=0.25))), - ) - self.connect(self.vanalog, self.ref_buf.pwr) - self.vcenter = self.connect(self.ref_rc.output) - - (self.reg_vcontrol, self.tp_vcontrol), _ = self.chain( - self.v5, - imp.Block(BoostConverter(output_voltage=(30, 33)*Volt, # up to but not greater - output_ripple_limit=1*mVolt)), - self.Block(VoltageTestPoint("vc+")) - ) - self.vcontrol = self.connect(self.reg_vcontrol.pwr_out) - - (self.filt_vcontroln, self.reg_vcontroln, self.tp_vcontroln), _ = self.chain( - self.vanalog, - self.Block(SeriesPowerFerriteBead()), - imp.Block(Lm2664(output_ripple_limit=5*mVolt)), - self.Block(VoltageTestPoint("vc-")) - ) - self.vcontroln = self.connect(self.reg_vcontroln.pwr_out.as_ground( - current_draw=self.reg_vcontrol.pwr_out.link().current_drawn)) - - # power path domain - with self.implicit_connect( - ImplicitConnect(self.vconv, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.control = imp.Block(SourceMeasureControl( - current=OUTPUT_CURRENT_RATING, - rds_on=(0, 0.2)*Ohm - )) - self.connect(self.vanalog, self.control.pwr_logic) - self.connect(self.vcenter, self.control.ref_center) - - self.connect(self.vcontroln, self.control.pwr_gate_neg) - self.connect(self.vcontrol, self.control.pwr_gate_pos) - - (self.tp_err, ), _ = self.chain(self.control.tp_err, imp.Block(AnalogCoaxTestPoint('err'))) - (self.tp_int, ), _ = self.chain(self.control.tp_int, imp.Block(AnalogCoaxTestPoint('int'))) - - # logic domain - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - # TODO next revision: optional clamping diode on CC lines (as present in PD buddy sink, but not OtterPill) - self.pd = imp.Block(Fusb302b()) - self.connect(self.vusb, self.pd.vbus) - self.connect(self.usb.cc, self.pd.cc) - - self.mcu = imp.Block(IoController()) - (self.led, ), _ = self.chain(imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request('led')) # debugging LED - - (self.usb_esd, ), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), - self.mcu.usb.request()) - - int_i2c = self.mcu.i2c.request('int_i2c') - self.i2c_tp = self.Block(I2cTestPoint('i2c')).connected(int_i2c) - (self.i2c_pull, ), _ = self.chain(int_i2c, imp.Block(I2cPullup())) - self.connect(int_i2c, self.pd.i2c, self.vusb_sense.i2c, self.convin_sense.i2c) - self.connect(self.mcu.gpio.request('pd_int'), self.pd.int) - - self.oled = imp.Block(Er_Oled022_1()) # (probably) pin compatible w/ 2.4" ER-OLED024-2B; maybe ER-OLED015-2B - self.connect(self.oled.vcc, self.v12) - self.connect(self.oled.pwr, self.v3v3) - self.oled_rc = imp.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10*mSecond(tol=0.2))).connected(io=self.oled.reset) - self.connect(int_i2c, self.oled.i2c) - # self.connect(self.mcu.spi.request('oled_spi'), self.oled.spi) - # self.connect(self.mcu.gpio.request('oled_cs'), self.oled.cs) - # self.connect(self.mcu.gpio.request('oled_dc'), self.oled.dc) - - # expander for low-speed control signals - self.ioe_ctl = imp.Block(Pca9554()) - self.connect(self.ioe_ctl.i2c, int_i2c) - self.connect(self.ioe_ctl.io.request('high_gate'), self.control.high_gate_ctl) - self.connect(self.ioe_ctl.io.request('low_gate'), self.control.low_gate_ctl) - self.connect(self.ioe_ctl.io.request_vector('off'), self.control.off) - (self.ramp_pull, ), _ = self.chain(self.ioe_ctl.io.request('ramp'), imp.Block(PulldownResistor(10*kOhm(tol=0.05))), - self.ramp.control) - - rc_model = DigitalLowPassRc(150*Ohm(tol=0.05), 7*MHertz(tol=0.2)) - (self.buck_rc, ), _ = self.chain(self.mcu.gpio.request('buck_pwm'), imp.Block(rc_model), self.conv.buck_pwm) - (self.boost_rc, ), _ = self.chain(self.mcu.gpio.request('boost_pwm'), imp.Block(rc_model), self.conv.boost_pwm) - - (self.conv_ovp, ), _ = self.chain(self.conv_outforce.pwr_out.as_analog_source(), - imp.Block(VoltageComparator(trip_voltage=(30, 40)*Volt))) - - self.conv_latch = imp.Block(SrLatchInverted()) - (self.conv_en_pull, ), _ = self.chain( - self.ioe_ctl.io.request('conv_en'), - imp.Block(PulldownResistor(10*kOhm(tol=0.05))), - self.conv_latch.rst - ) - (self.comp_pull, ), _ = self.chain( - self.conv_ovp.output, imp.Block(PullupResistor(resistance=10*kOhm(tol=0.05))), - self.conv_latch.set - ) - self.connect(self.conv_latch.out, self.conv.reset, self.ioe_ctl.io.request('conv_en_sense')) - - (self.pass_temp, ), _ = self.chain(int_i2c, imp.Block(Tmp1075n(0))) - (self.conv_temp, ), _ = self.chain(int_i2c, imp.Block(Tmp1075n(1))) - (self.conv_sense, ), _ = self.chain( - self.vconv, - imp.Block(VoltageSenseDivider(full_scale_voltage=2.2*Volt(tol=0.1), impedance=(1, 10)*kOhm)), - self.mcu.adc.request('vconv_sense') - ) - - # expander and interface elements - self.ioe_ui = imp.Block(Pca9554(addr_lsb=2)) - self.connect(self.ioe_ui.i2c, int_i2c) - self.enc = imp.Block(DigitalRotaryEncoder()) - self.connect(self.enc.a, self.mcu.gpio.request('enc_a')) - self.connect(self.enc.b, self.mcu.gpio.request('enc_b')) - self.connect(self.enc.with_mixin(DigitalRotaryEncoderSwitch()).sw, self.mcu.gpio.request('enc_sw')) - self.dir = imp.Block(DigitalDirectionSwitch()) - self.connect(self.dir.a, self.ioe_ui.io.request('dir_a')) - self.connect(self.dir.b, self.ioe_ui.io.request('dir_b')) - self.connect(self.dir.c, self.ioe_ui.io.request('dir_c')) - self.connect(self.dir.d, self.ioe_ui.io.request('dir_d')) - self.connect(self.dir.with_mixin(DigitalDirectionSwitchCenter()).center, self.ioe_ui.io.request('dir_cen')) - self.connect(self.ioe_ui.io.request_vector('irange'), self.control.irange) - - # expansion ports - (self.qwiic_pull, self.qwiic, ), _ = self.chain(self.mcu.i2c.request('qwiic'), - imp.Block(I2cPullup()), - imp.Block(QwiicTarget())) - - self.dutio = imp.Block(SourceMeasureDutConnector()) - (self.dut0_clamp, ), _ = self.chain(self.dutio.io0, - self.Block(DigitalClampResistor(protection_voltage=(0, 30)*Volt)), - self.mcu.gpio.request('dut0')) - (self.dut1_clamp, ), _ = self.chain(self.dutio.io1, - self.Block(DigitalClampResistor(protection_voltage=(0, 30)*Volt)), - self.mcu.gpio.request('dut1')) - - mcu_touch = self.mcu.with_mixin(IoControllerTouchDriver()) - (self.touch_duck, ), _ = self.chain( - mcu_touch.touch.request('touch_duck'), - imp.Block(FootprintToucbPad('edg:Symbol_DucklingSolid')) - ) - - # 5v domain - with self.implicit_connect( + @override + def contents(self) -> None: + super().contents() + + # overall design parameters + OUTPUT_CURRENT_RATING = (0, 3) * Amp + + # USB PD port that supplies power to the load + # USB PD can't actually do 8 A, but this suppresses the error and we can software-limit current draw + self.usb = self.Block(UsbCReceptacle(voltage_out=(5, 20) * Volt, current_limits=(0, 8) * Amp)) + + self.gnd = self.connect(self.usb.gnd) + self.tp_gnd = self.Block(GroundTestPoint()).connected(self.usb.gnd) + + # power supplies + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.vusb_sense = imp.Block(Ina219(10 * mOhm(tol=0.01))) + + # input filtering and protection + (self.fuse_vusb, self.filt_vusb, self.prot_vusb), _ = self.chain( + self.usb.pwr, + self.Block(SeriesPowerFuse(trip_current=(7, 8) * Amp)), + self.Block(SeriesPowerFerriteBead()), + imp.Block(ProtectionZenerDiode(voltage=(32, 40) * Volt)), # for parts commonality w/ the Vconv zener + self.vusb_sense.sense_pos, + ) + self.vusb = self.connect(self.vusb_sense.sense_neg) + + (self.ramp, self.cap_conv), _ = self.chain( + self.vusb, + imp.Block( + RampLimiter(target_vgs=(3.7, 19) * Volt) + ), # avoid excess capacitance on VBus which may cause the PD source to reset + imp.Block(DecouplingCapacitor(47 * uFarad(tol=0.25))), + ) + self.vusb_ramp = self.connect(self.ramp.pwr_out) # vusb post-ramp + self.tp_vusb = self.Block(VoltageTestPoint()).connected(self.ramp.pwr_out) + + # logic supplies + (self.reg_v5, self.tp_v5), _ = self.chain( + self.vusb_ramp, # non-critical power supply downstream of ramp limiter + imp.Block(BuckConverter(output_voltage=5 * Volt(tol=0.05))), # min set by gate drivers + self.Block(VoltageTestPoint()), + ) + self.v5 = self.connect(self.reg_v5.pwr_out) + + (self.reg_3v3, self.prot_3v3, self.tp_3v3), _ = self.chain( + self.vusb, # upstream of ramp limiter, required for bootstrapping + imp.Block(BuckConverter(output_voltage=3.3 * Volt(tol=0.05))), + imp.Block(ProtectionZenerDiode(voltage=(3.6, 4.5) * Volt)), + self.Block(VoltageTestPoint()), + ) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + self.connect(self.vusb_sense.pwr, self.v3v3) + + (self.reg_v12,), _ = self.chain( + self.v5, + imp.Block(BoostConverter(output_voltage=(12, 13) * Volt)), # limits of the OLED + ) + self.v12 = self.connect(self.reg_v12.pwr_out) + + # output power supplies + self.convin_sense = imp.Block(Ina219(10 * mOhm(tol=0.01), addr_lsb=4)) + self.connect(self.convin_sense.pwr, self.v3v3) + self.connect(self.vusb_ramp, self.convin_sense.sense_pos) + self.vconvin = self.connect(self.convin_sense.sense_neg) + (self.conv_inforce, self.conv, self.conv_outforce, self.prot_conv, self.tp_conv), _ = self.chain( + self.vconvin, + imp.Block(ForcedVoltage(20 * Volt(tol=0))), # assumed input voltage to target buck-boost ratios + imp.Block( + CustomSyncBuckBoostConverterPwm( + output_voltage=(15, 30) * Volt, # design for 0.5x - 1.5x conv ratio + frequency=500 * kHertz(tol=0), + ripple_ratio=(0.01, 0.9), + input_ripple_limit=(100 * (6 / 7)) * mVolt, # fill empty space with caps + output_ripple_limit=(25 * (7 / 8)) * mVolt, # fill empty space with caps + ) + ), + imp.Block(ForcedVoltage((2, 30) * Volt)), # at least 2v to allow current sensor to work + imp.Block( + ProtectionZenerDiode(voltage=(32, 40) * Volt) + ), # zener shunt in case the boost converter goes crazy + self.Block(VoltageTestPoint()), + ) + self.connect(self.conv.pwr_logic, self.v5) + self.vconv = self.connect(self.conv_outforce.pwr_out) + + # analog supplies + (self.reg_analog, self.tp_analog), _ = self.chain( + self.v5, + imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))), + self.Block(VoltageTestPoint("va")), + ) + self.vanalog = self.connect(self.reg_analog.pwr_out) + + (self.reg_vref, self.tp_vref), _ = self.chain( + self.v5, + imp.Block(VoltageReference(output_voltage=3.3 * Volt(tol=0.01))), + self.Block(VoltageTestPoint()), + ) + self.vref = self.connect(self.reg_vref.pwr_out) + + (self.ref_div, self.ref_buf, self.ref_rc), _ = self.chain( + self.vref, + imp.Block(VoltageDivider(output_voltage=1.65 * Volt(tol=0.05), impedance=(10, 100) * kOhm)), + imp.Block(OpampFollower()), + # opamp outputs generally not stable under capacitive loading and requires an isolation series resistor + # 4.7 tries to balance low output impedance and some level of isolation + imp.Block(AnalogLowPassRc(4.7 * Ohm(tol=0.05), 1 * MHertz(tol=0.25))), + ) + self.connect(self.vanalog, self.ref_buf.pwr) + self.vcenter = self.connect(self.ref_rc.output) + + (self.reg_vcontrol, self.tp_vcontrol), _ = self.chain( + self.v5, + imp.Block( + BoostConverter( + output_voltage=(30, 33) * Volt, output_ripple_limit=1 * mVolt # up to but not greater + ) + ), + self.Block(VoltageTestPoint("vc+")), + ) + self.vcontrol = self.connect(self.reg_vcontrol.pwr_out) + + (self.filt_vcontroln, self.reg_vcontroln, self.tp_vcontroln), _ = self.chain( + self.vanalog, + self.Block(SeriesPowerFerriteBead()), + imp.Block(Lm2664(output_ripple_limit=5 * mVolt)), + self.Block(VoltageTestPoint("vc-")), + ) + self.vcontroln = self.connect( + self.reg_vcontroln.pwr_out.as_ground(current_draw=self.reg_vcontrol.pwr_out.link().current_drawn) + ) + + # power path domain + with self.implicit_connect( + ImplicitConnect(self.vconv, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.control = imp.Block(SourceMeasureControl(current=OUTPUT_CURRENT_RATING, rds_on=(0, 0.2) * Ohm)) + self.connect(self.vanalog, self.control.pwr_logic) + self.connect(self.vcenter, self.control.ref_center) + + self.connect(self.vcontroln, self.control.pwr_gate_neg) + self.connect(self.vcontrol, self.control.pwr_gate_pos) + + (self.tp_err,), _ = self.chain(self.control.tp_err, imp.Block(AnalogCoaxTestPoint("err"))) + (self.tp_int,), _ = self.chain(self.control.tp_int, imp.Block(AnalogCoaxTestPoint("int"))) + + # logic domain + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + # TODO next revision: optional clamping diode on CC lines (as present in PD buddy sink, but not OtterPill) + self.pd = imp.Block(Fusb302b()) + self.connect(self.vusb, self.pd.vbus) + self.connect(self.usb.cc, self.pd.cc) + + self.mcu = imp.Block(IoController()) + (self.led,), _ = self.chain( + imp.Block(IndicatorSinkLed(Led.Red)), self.mcu.gpio.request("led") + ) # debugging LED + + (self.usb_esd,), self.usb_chain = self.chain(self.usb.usb, imp.Block(UsbEsdDiode()), self.mcu.usb.request()) + + int_i2c = self.mcu.i2c.request("int_i2c") + self.i2c_tp = self.Block(I2cTestPoint("i2c")).connected(int_i2c) + (self.i2c_pull,), _ = self.chain(int_i2c, imp.Block(I2cPullup())) + self.connect(int_i2c, self.pd.i2c, self.vusb_sense.i2c, self.convin_sense.i2c) + self.connect(self.mcu.gpio.request("pd_int"), self.pd.int) + + self.oled = imp.Block( + Er_Oled022_1() + ) # (probably) pin compatible w/ 2.4" ER-OLED024-2B; maybe ER-OLED015-2B + self.connect(self.oled.vcc, self.v12) + self.connect(self.oled.pwr, self.v3v3) + self.oled_rc = imp.Block(PullupDelayRc(10 * kOhm(tol=0.05), 10 * mSecond(tol=0.2))).connected( + io=self.oled.reset + ) + self.connect(int_i2c, self.oled.i2c) + # self.connect(self.mcu.spi.request('oled_spi'), self.oled.spi) + # self.connect(self.mcu.gpio.request('oled_cs'), self.oled.cs) + # self.connect(self.mcu.gpio.request('oled_dc'), self.oled.dc) + + # expander for low-speed control signals + self.ioe_ctl = imp.Block(Pca9554()) + self.connect(self.ioe_ctl.i2c, int_i2c) + self.connect(self.ioe_ctl.io.request("high_gate"), self.control.high_gate_ctl) + self.connect(self.ioe_ctl.io.request("low_gate"), self.control.low_gate_ctl) + self.connect(self.ioe_ctl.io.request_vector("off"), self.control.off) + (self.ramp_pull,), _ = self.chain( + self.ioe_ctl.io.request("ramp"), imp.Block(PulldownResistor(10 * kOhm(tol=0.05))), self.ramp.control + ) + + rc_model = DigitalLowPassRc(150 * Ohm(tol=0.05), 7 * MHertz(tol=0.2)) + (self.buck_rc,), _ = self.chain(self.mcu.gpio.request("buck_pwm"), imp.Block(rc_model), self.conv.buck_pwm) + (self.boost_rc,), _ = self.chain( + self.mcu.gpio.request("boost_pwm"), imp.Block(rc_model), self.conv.boost_pwm + ) + + (self.conv_ovp,), _ = self.chain( + self.conv_outforce.pwr_out.as_analog_source(), + imp.Block(VoltageComparator(trip_voltage=(30, 40) * Volt)), + ) + + self.conv_latch = imp.Block(SrLatchInverted()) + (self.conv_en_pull,), _ = self.chain( + self.ioe_ctl.io.request("conv_en"), + imp.Block(PulldownResistor(10 * kOhm(tol=0.05))), + self.conv_latch.rst, + ) + (self.comp_pull,), _ = self.chain( + self.conv_ovp.output, imp.Block(PullupResistor(resistance=10 * kOhm(tol=0.05))), self.conv_latch.set + ) + self.connect(self.conv_latch.out, self.conv.reset, self.ioe_ctl.io.request("conv_en_sense")) + + (self.pass_temp,), _ = self.chain(int_i2c, imp.Block(Tmp1075n(0))) + (self.conv_temp,), _ = self.chain(int_i2c, imp.Block(Tmp1075n(1))) + (self.conv_sense,), _ = self.chain( + self.vconv, + imp.Block(VoltageSenseDivider(full_scale_voltage=2.2 * Volt(tol=0.1), impedance=(1, 10) * kOhm)), + self.mcu.adc.request("vconv_sense"), + ) + + # expander and interface elements + self.ioe_ui = imp.Block(Pca9554(addr_lsb=2)) + self.connect(self.ioe_ui.i2c, int_i2c) + self.enc = imp.Block(DigitalRotaryEncoder()) + self.connect(self.enc.a, self.mcu.gpio.request("enc_a")) + self.connect(self.enc.b, self.mcu.gpio.request("enc_b")) + self.connect(self.enc.with_mixin(DigitalRotaryEncoderSwitch()).sw, self.mcu.gpio.request("enc_sw")) + self.dir = imp.Block(DigitalDirectionSwitch()) + self.connect(self.dir.a, self.ioe_ui.io.request("dir_a")) + self.connect(self.dir.b, self.ioe_ui.io.request("dir_b")) + self.connect(self.dir.c, self.ioe_ui.io.request("dir_c")) + self.connect(self.dir.d, self.ioe_ui.io.request("dir_d")) + self.connect(self.dir.with_mixin(DigitalDirectionSwitchCenter()).center, self.ioe_ui.io.request("dir_cen")) + self.connect(self.ioe_ui.io.request_vector("irange"), self.control.irange) + + # expansion ports + ( + self.qwiic_pull, + self.qwiic, + ), _ = self.chain(self.mcu.i2c.request("qwiic"), imp.Block(I2cPullup()), imp.Block(QwiicTarget())) + + self.dutio = imp.Block(SourceMeasureDutConnector()) + (self.dut0_clamp,), _ = self.chain( + self.dutio.io0, + self.Block(DigitalClampResistor(protection_voltage=(0, 30) * Volt)), + self.mcu.gpio.request("dut0"), + ) + (self.dut1_clamp,), _ = self.chain( + self.dutio.io1, + self.Block(DigitalClampResistor(protection_voltage=(0, 30) * Volt)), + self.mcu.gpio.request("dut1"), + ) + + mcu_touch = self.mcu.with_mixin(IoControllerTouchDriver()) + (self.touch_duck,), _ = self.chain( + mcu_touch.touch.request("touch_duck"), imp.Block(FootprintToucbPad("edg:Symbol_DucklingSolid")) + ) + + # 5v domain + with self.implicit_connect( ImplicitConnect(self.v5, [Power]), ImplicitConnect(self.gnd, [Common]), - ) as imp: - (self.rgbs, ), _ = self.chain( - self.mcu.gpio.request('rgb'), - imp.Block(NeopixelArray(4+1+1)) # 4 for encoder, 1 for output, 1 for USB - ) - - self.fan_cap = imp.Block(DecouplingCapacitor(1*uFarad(tol=0.2))) - self.fan_drv = imp.Block(HighSideSwitch()) - self.connect(self.mcu.gpio.request('fan'), self.fan_drv.control) - self.fan = self.Block(SourceMeasureFan()) - self.connect(self.fan.gnd, self.gnd) - self.connect(self.fan.pwr, self.fan_drv.output) - - (self.spk_drv, self.spk), _ = self.chain( - self.mcu.with_mixin(IoControllerI2s()).i2s.request('speaker'), - imp.Block(Max98357a()), - self.Block(Speaker()) - ) - - # analog domain - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.dac = imp.Block(Mcp4728()) - (self.dac_ferrite, ), _ = self.chain( - self.vref, - imp.Block(SeriesPowerFerriteBead(Range.from_lower(1000))), - self.dac.pwr) - self.connect(self.dac.out0, self.control.control_voltage) - self.connect(self.dac.out2, self.control.control_voltage_fine) - self.connect(self.dac.out1, self.control.control_current_sink) - self.connect(self.dac.out3, self.control.control_current_source) - self.connect(self.dac.i2c, int_i2c) - - self.adc = imp.Block(Mcp3561()) - self.connect(self.adc.pwra, self.vanalog) - self.connect(self.adc.pwr, self.v3v3) - self.connect(self.adc.vref, self.vref) - self.connect(self.adc.spi, self.mcu.spi.request('adc_spi')) - self.connect(self.adc.cs, self.mcu.gpio.request('adc_cs')) - self.connect(self.adc.mclkin, self.mcu.gpio.request('adc_clk')) # up to 20MHz output from LEDC peripheral - (self.tp_vcen, self.vcen_rc, ), _ = self.chain(self.vcenter, - imp.Block(AnalogCoaxTestPoint('cen')), - imp.Block(AnalogLowPassRc(1*kOhm(tol=0.05), 16*kHertz(tol=0.25))), - self.adc.vins.request('2')) - (self.tp_mv, self.mv_rc, ), _ = self.chain(self.control.measured_voltage, - imp.Block(AnalogCoaxTestPoint('mv')), - imp.Block(AnalogLowPassRc(1*kOhm(tol=0.05), 16*kHertz(tol=0.25))), - self.adc.vins.request('0')) - (self.tp_mi, self.mi_rc, ), _ = self.chain(self.control.measured_current, - imp.Block(AnalogCoaxTestPoint('mi')), - imp.Block(AnalogLowPassRc(1*kOhm(tol=0.05), 16*kHertz(tol=0.25))), - self.adc.vins.request('1')) - self.connect(self.control.limit_source, self.mcu.gpio.request('limit_source')) - self.connect(self.control.limit_sink, self.mcu.gpio.request('limit_sink')) - - self.outn = self.Block(BananaSafetyJack()) - self.outp = self.Block(BananaSafetyJack()) - self.outd = self.Block(PinHeader254Horizontal(2)) # 2.54 output option - self.connect(self.gnd, self.outn.port.adapt_to(Ground()), self.outd.pins.request('1').adapt_to(Ground())) - self.connect( - self.control.out, - self.outp.port.adapt_to(VoltageSink(current_draw=OUTPUT_CURRENT_RATING)), - self.outd.pins.request('2').adapt_to(VoltageSink()) - ) - - self._block_diagram_grouping = self.Metadata({ - 'pwr': 'usb, filt_vusb, fuse_vusb, prot_vusb, pd, vusb_sense, reg_v5, reg_3v3, prot_3v3', - 'conv': 'conv_inforce, ramp, convin_sense, cap_conv, conv, conv_outforce, conv_sense, prot_conv, ' - 'conv_en_pull, conv_latch, conv_ovp, comp_pull, buck_rc, boost_rc', - 'analog': 'reg_analog, reg_vcontrol, reg_vcontroln, reg_vref, ref_div, ref_buf, ref_cap, vcen_rc, ' - 'dac_ferrite, dac, mv_rc, mi_rc, adc, control, ' - 'outn, outp, outd', - 'mcu': 'mcu, led, touch_duck, ioe_ctl, usb_esd, i2c_pull, qwiic_pull, qwiic, dutio', - 'sensing': 'conv_temp, pass_temp', - 'ui': 'ioe_ui, enc, dir, rgb, reg_v12, oled, oled_rc, spk_drv, spk', - 'tp': 'tp_vusb, tp_gnd, tp_3v3, tp_v5, tp_v12, tp_conv, tp_analog, tp_vcontrol, tp_vcontroln, tp_vref, tp_lsrc, tp_lsnk, ' - 'i2c_tp', - 'rf_tp': 'tp_vcen, tp_cv, tp_cvf, tp_cisrc, tp_cisnk, tp_mv, tp_mi', - 'packed_amps': 'vimeas_amps, ampdmeas_amps, cv_amps, ci_amps, cintref_amps', - 'misc': 'fan_drv, fan, jlc_th', - }) - - @override - def multipack(self) -> None: - self.vimeas_amps = self.PackedBlock(Opa2189()) # low noise opamp - self.pack(self.vimeas_amps.elements.request('0'), ['control', 'amp', 'amp']) - self.pack(self.vimeas_amps.elements.request('1'), ['control', 'hvbuf', 'amp']) - - self.cv_amps = self.PackedBlock(Tlv9152()) - self.pack(self.cv_amps.elements.request('0'), ['ref_buf', 'amp']) # place the reference more centrally - self.pack(self.cv_amps.elements.request('1'), ['control', 'err_volt', 'amp']) - - self.ci_amps = self.PackedBlock(Tlv9152()) - self.pack(self.ci_amps.elements.request('0'), ['control', 'err_sink', 'amp']) - self.pack(self.ci_amps.elements.request('1'), ['control', 'err_source', 'amp']) - - self.cintref_amps = self.PackedBlock(Tlv9152()) - self.pack(self.cintref_amps.elements.request('0'), ['control', 'int', 'amp']) - self.pack(self.cintref_amps.elements.request('1'), ['control', 'dmeas', 'amp']) # this path matters much less - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['mcu'], Esp32s3_Wroom_1), - (['reg_v5'], Tps54202h), - (['reg_3v3'], Tps54202h), - (['reg_v12'], Lm2733), - (['reg_analog'], Ap2210), - (['reg_vref'], Ref30xx), - (['reg_vcontrol'], Lm2733), - - (['control', 'driver', 'low_fet'], CustomFet), - (['control', 'driver', 'high_fet'], CustomFet), - - (['control', 'off_sw', 'device'], Sn74lvc1g3157), - - (['cap_conv', 'cap'], JlcAluminumCapacitor), - (['control', 'driver', 'cap_in1', 'cap'], JlcAluminumCapacitor), - - (['spk', 'conn'], JstPhKVertical), - - (['control', 'isense', 'ranges[0]', 'pwr_sw', 'ic'], Tlp3545a), # higher current on 3A range - (['control', 'driver', 'res'], SeriesResistor), # needed for high power within a basic part - (['oled', 'device', 'conn'], Fpc050BottomFlip), # more compact connector, double-fold the FPC ribbon - ], - class_refinements=[ - (EspProgrammingHeader, EspProgrammingTc2030), - (TagConnect, TagConnectNonLegged), # really for initial flash / emergency upload only - (Opamp, Tlv9061), # higher precision opamps - (AnalogSwitch, Dg468), - (SolidStateRelay, Tlp170am), - (BananaSafetyJack, Ct3151), - (HalfBridgeDriver, Ncp3420), - (DirectionSwitch, Skrh), - (TestPoint, CompactKeystone5015), - (RotaryEncoder, Pec11s), - (Neopixel, Ws2812c_2020), - (RfConnector, UflConnector), - (UsbEsdDiode, Pgb102st23), # in stock - ], - instance_values=[ - (['mcu', 'programming'], 'uart-auto'), - (['mcu', 'pin_assigns'], [ - # left side - 'speaker.sd=4', - 'speaker.sck=5', - 'speaker.ws=6', - 'int_i2c.sda=7', - 'int_i2c.scl=8', - 'pd_int=9', - 'buck_pwm=10', - 'boost_pwm=11', - 'vconv_sense=12', - - # bottom side - 'adc_cs=15', - 'adc_spi.sck=17', - 'adc_spi.mosi=18', - 'adc_spi.miso=19', - 'adc_clk=20', - 'touch_duck=21', - 'limit_source=22', - 'limit_sink=23', - 'dut0=24', - 'dut1=25', - - # right side - 'rgb=31', - 'fan=32', - 'enc_sw=33', - 'enc_b=34', - 'enc_a=35', - 'qwiic.sda=38', - 'qwiic.scl=39', - - 'led=_GPIO0_STRAP', - ]), - - (['ioe_ui', 'pin_assigns'], [ - 'dir_a=4', - 'dir_cen=5', - 'dir_c=6', - 'dir_d=7', - 'dir_b=12', - 'irange_1=9', - 'irange_2=10', - 'irange_0=11', - ]), - - (['ioe_ctl', 'pin_assigns'], [ - 'ramp=4', - 'conv_en=6', - 'conv_en_sense=7', - - 'low_gate=12', - 'high_gate=11', - 'off_0=9', - ]), - - # allow the regulator to go into tracking mode - (['reg_v5', 'power_path', 'dutycycle_limit'], Range(0, float('inf'))), - (['reg_v5', 'power_path', 'inductor_current_ripple'], Range(0.01, 0.5)), # trade higher Imax for lower L - - # JLC does not have frequency specs, must be checked TODO - (['reg_3v3', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - (['reg_v5', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - (['reg_v12', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - (['conv', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - (['reg_vcontrol', 'power_path', 'inductor', 'manual_frequency_rating'], Range.all()), - (['vusb_sense', 'Rs', 'res', 'res', 'footprint_spec'], "Resistor_SMD:R_1206_3216Metric"), - (['convin_sense', 'Rs', 'res', 'res', 'footprint_spec'], ParamValue(['vusb_sense', 'Rs', 'res', 'res', 'footprint_spec'])), - - (['ramp', 'drv', 'footprint_spec'], 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic'), - - # less aggressive derating for smaller part - (['control', 'driver', 'cap_in1', 'cap', 'voltage_rating_derating'], 0.8), - - # ignore derating on 20v - it's really broken =( - (['reg_v5', 'power_path', 'in_cap', 'cap', 'exact_capacitance'], False), - (['reg_v5', 'power_path', 'in_cap', 'cap', 'voltage_rating_derating'], 0.85), - (['reg_v12', 'cf', 'voltage_rating_derating'], 0.85), - (['conv', 'power_path', 'in_cap', 'cap', 'exact_capacitance'], False), - (['conv', 'power_path', 'in_cap', 'cap', 'voltage_rating_derating'], 0.85), - (['conv', 'power_path', 'out_cap', 'cap', 'exact_capacitance'], False), - (['conv', 'power_path', 'out_cap', 'cap', 'voltage_rating_derating'], 0.9), # allow using a 35V cap - (['conv', 'power_path', 'out_cap', 'cap', 'require_basic_part'], False), - (['control', 'driver', 'cap_in2', 'cap', 'voltage_rating_derating'], 0.9), - (['control', 'driver', 'cap_in3', 'cap', 'voltage_rating_derating'], 0.9), - (['reg_vcontrol', 'cf', 'voltage_rating_derating'], 0.85), - (['reg_vcontrol', 'power_path', 'out_cap', 'cap', 'exact_capacitance'], False), - (['reg_vcontrol', 'power_path', 'out_cap', 'cap', 'voltage_rating_derating'], 0.85), - (['conv', 'boost_sw', 'high_fet', 'gate_voltage'], ParamValue( - ['conv', 'boost_sw', 'low_fet', 'gate_voltage'] - )), # TODO model is broken for unknown reasons - (['boot', 'c_fly_pos', 'voltage_rating_derating'], 0.85), - (['boot', 'c_fly_neg', 'voltage_rating_derating'], 0.85), - (['conv', 'boost_sw', 'low_fet', 'footprint_spec'], 'Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic'), - (['conv', 'boost_sw', 'low_fet', 'part'], "BSC093N04LSG"), # lower total power - # require all FETs to be the same; note boost must elaborate first - (['conv', 'buck_sw', 'low_fet', 'part'], ParamValue(['conv', 'boost_sw', 'low_fet', 'actual_part'])), - (['conv', 'buck_sw', 'high_fet', 'part'], ParamValue(['conv', 'boost_sw', 'low_fet', 'actual_part'])), - (['conv', 'boost_sw', 'high_fet', 'part'], ParamValue(['conv', 'boost_sw', 'low_fet', 'actual_part'])), - (['conv', 'boost_sw', 'gate_res'], Range.from_tolerance(4.7, 0.05)), - (['conv', 'buck_sw', 'gate_res'], ParamValue(['conv', 'boost_sw', 'gate_res'])), - - (['control', 'int_link', 'sink_impedance'], RangeExpr.INF), # waive impedance check for integrator in - - (['control', 'isense', 'ranges[0]', 'isense', 'res', 'res', 'require_basic_part'], False), - (['control', 'isense', 'ranges[1]', 'isense', 'res', 'res', 'require_basic_part'], False), - (['control', 'isense', 'ranges[2]', 'isense', 'res', 'res', 'require_basic_part'], False), - - (['control', 'driver', 'res', 'count'], 3), - (['control', 'driver', 'high_fet', 'footprint_spec'], 'Package_TO_SOT_THT:TO-220-3_Horizontal_TabUp'), - (['control', 'driver', 'high_fet', 'part_spec'], 'IRF540N'), - (['control', 'driver', 'low_fet', 'footprint_spec'], 'Package_TO_SOT_THT:TO-220-3_Horizontal_TabUp'), - (['control', 'driver', 'low_fet', 'part_spec'], 'IRF9540'), # has a 30V/4A SOA - - (['prot_vusb', 'diode', 'footprint_spec'], 'Diode_SMD:D_SMA'), - (['prot_conv', 'diode', 'footprint_spec'], 'Diode_SMD:D_SMA'), - (['prot_3v3', 'diode', 'footprint_spec'], 'Diode_SMD:D_SMA'), - - # reduce maximum SSR drive current to be within the IO expander limit - (['control', 'isense', 'ranges[0]', 'pwr_sw', 'ic', 'led_current_recommendation'], Range(0.002, 0.010)), - (['control', 'isense', 'ranges[1]', 'pwr_sw', 'ic', 'led_current_recommendation'], Range(0.002, 0.010)), - (['control', 'isense', 'ranges[2]', 'pwr_sw', 'ic', 'led_current_recommendation'], Range(0.002, 0.010)), - (['spk_drv', 'pwr', 'current_draw'], Range(6.0e-7, 0.25)), # assume speakers will be pretty mild - - # the basic parts in the 100kOhm range is pretty limited - (['ramp', 'div', 'bottom_res', 'require_basic_part'], False), - (['reg_v5', 'fb', 'div', 'top_res', 'require_basic_part'], False), - (['reg_v12', 'fb', 'div', 'top_res', 'require_basic_part'], False), - (['reg_vcontrol', 'fb', 'div', 'top_res', 'require_basic_part'], False), - (['reg_vcontrol', 'fb', 'div', 'bottom_res', 'require_basic_part'], False), - - # use more basic parts - (['control', 'snub_c', 'cap', 'voltage_rating_derating'], 0.60), # allow use of a 50V basic part caps - (['control', 'ifilt', 'c', 'voltage_rating_derating'], 0.60), - - # note, can't limit 5v reg and 12v reg feedback series, no overlap w/ downstream part supply voltages - (['ramp', 'div', 'series'], 6), - (['oled', 'iref_res', 'resistance'], Range.from_tolerance(1e6, 0.05)), # use 1M resistor, up from 910k - (['ramp', 'drv', 'gate_voltage'], Range(0.0, 10.0)), # gate max isn't parsed, but typically up to 20v - - # reduce line items - (['control', 'err_source', 'diode', 'part'], ParamValue(['reg_v12', 'rect', 'actual_part'])), - (['control', 'err_sink', 'diode', 'part'], ParamValue(['reg_v12', 'rect', 'actual_part'])), - (['reg_vcontrol', 'rect', 'part'], ParamValue(['reg_v12', 'rect', 'actual_part'])), - (['conv', 'buck_sw', 'driver', 'boot', 'part'], ParamValue(['reg_v12', 'rect', 'actual_part'])), - (['conv', 'boost_sw', 'driver', 'boot', 'part'], ParamValue(['reg_v12', 'rect', 'actual_part'])), - - (['convin_sense', 'Rs', 'res', 'res', 'part'], ParamValue(['vusb_sense', 'Rs', 'res', 'res', 'part'])), - (['vusb_sense', 'Rs', 'res', 'res', 'require_basic_part'], False), - (['convin_sense', 'Rs', 'res', 'res', 'require_basic_part'], False), - (['reg_3v3', 'power_path', 'inductor', 'part'], ParamValue(['reg_v5', 'power_path', 'inductor', 'actual_part'])), - (['reg_vcontrol', 'power_path', 'inductor', 'part'], ParamValue(['reg_v12', 'power_path', 'inductor', 'actual_part'])), - (['filt_vcontroln', 'fb', 'part'], ParamValue(['dac_ferrite', 'fb', 'actual_part'])), - # the low-leakage TVS diodes aren't in the parts table, so directly insert the LCSC part - (['prot_vusb', 'diode', 'lcsc_part'], ParamValue(['control', 'tvs_p', 'lcsc_part'])), - (['prot_conv', 'diode', 'lcsc_part'], ParamValue(['control', 'tvs_p', 'lcsc_part'])), - (['prot_3v3', 'diode', 'lcsc_part'], ParamValue(['control', 'tvs_n', 'lcsc_part'])), # note, 5v zener diode - - # out of stock / unassembleable parts - (['conv', 'power_path', 'out_cap', 'cap', 'part'], "C3216X5R1V226MTJ00E"), - ], - class_values=[ - (CompactKeystone5015, ['lcsc_part'], 'C5199798'), - - (Mcp3561, ['ic', 'ch', '0', 'impedance'], Range(260e3, 510e3)), # GAIN=1 or lower - (Mcp3561, ['ic', 'ch', '1', 'impedance'], Range(260e3, 510e3)), # GAIN=1 or lower - (Mcp3561, ['ic', 'ch', '2', 'impedance'], Range(260e3, 510e3)), # GAIN=1 or lower - (Mcp3561, ['ic', 'ch', '3', 'impedance'], Range(260e3, 510e3)), # GAIN=1 or lower - - (Ws2812c_2020, ['device', 'lcsc_part'], 'C3646929'), # similar device suitable for economic assembly - ] - ) + ) as imp: + (self.rgbs,), _ = self.chain( + self.mcu.gpio.request("rgb"), + imp.Block(NeopixelArray(4 + 1 + 1)), # 4 for encoder, 1 for output, 1 for USB + ) + + self.fan_cap = imp.Block(DecouplingCapacitor(1 * uFarad(tol=0.2))) + self.fan_drv = imp.Block(HighSideSwitch()) + self.connect(self.mcu.gpio.request("fan"), self.fan_drv.control) + self.fan = self.Block(SourceMeasureFan()) + self.connect(self.fan.gnd, self.gnd) + self.connect(self.fan.pwr, self.fan_drv.output) + + (self.spk_drv, self.spk), _ = self.chain( + self.mcu.with_mixin(IoControllerI2s()).i2s.request("speaker"), + imp.Block(Max98357a()), + self.Block(Speaker()), + ) + + # analog domain + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.dac = imp.Block(Mcp4728()) + (self.dac_ferrite,), _ = self.chain( + self.vref, imp.Block(SeriesPowerFerriteBead(Range.from_lower(1000))), self.dac.pwr + ) + self.connect(self.dac.out0, self.control.control_voltage) + self.connect(self.dac.out2, self.control.control_voltage_fine) + self.connect(self.dac.out1, self.control.control_current_sink) + self.connect(self.dac.out3, self.control.control_current_source) + self.connect(self.dac.i2c, int_i2c) + + self.adc = imp.Block(Mcp3561()) + self.connect(self.adc.pwra, self.vanalog) + self.connect(self.adc.pwr, self.v3v3) + self.connect(self.adc.vref, self.vref) + self.connect(self.adc.spi, self.mcu.spi.request("adc_spi")) + self.connect(self.adc.cs, self.mcu.gpio.request("adc_cs")) + self.connect(self.adc.mclkin, self.mcu.gpio.request("adc_clk")) # up to 20MHz output from LEDC peripheral + ( + self.tp_vcen, + self.vcen_rc, + ), _ = self.chain( + self.vcenter, + imp.Block(AnalogCoaxTestPoint("cen")), + imp.Block(AnalogLowPassRc(1 * kOhm(tol=0.05), 16 * kHertz(tol=0.25))), + self.adc.vins.request("2"), + ) + ( + self.tp_mv, + self.mv_rc, + ), _ = self.chain( + self.control.measured_voltage, + imp.Block(AnalogCoaxTestPoint("mv")), + imp.Block(AnalogLowPassRc(1 * kOhm(tol=0.05), 16 * kHertz(tol=0.25))), + self.adc.vins.request("0"), + ) + ( + self.tp_mi, + self.mi_rc, + ), _ = self.chain( + self.control.measured_current, + imp.Block(AnalogCoaxTestPoint("mi")), + imp.Block(AnalogLowPassRc(1 * kOhm(tol=0.05), 16 * kHertz(tol=0.25))), + self.adc.vins.request("1"), + ) + self.connect(self.control.limit_source, self.mcu.gpio.request("limit_source")) + self.connect(self.control.limit_sink, self.mcu.gpio.request("limit_sink")) + + self.outn = self.Block(BananaSafetyJack()) + self.outp = self.Block(BananaSafetyJack()) + self.outd = self.Block(PinHeader254Horizontal(2)) # 2.54 output option + self.connect(self.gnd, self.outn.port.adapt_to(Ground()), self.outd.pins.request("1").adapt_to(Ground())) + self.connect( + self.control.out, + self.outp.port.adapt_to(VoltageSink(current_draw=OUTPUT_CURRENT_RATING)), + self.outd.pins.request("2").adapt_to(VoltageSink()), + ) + + self._block_diagram_grouping = self.Metadata( + { + "pwr": "usb, filt_vusb, fuse_vusb, prot_vusb, pd, vusb_sense, reg_v5, reg_3v3, prot_3v3", + "conv": "conv_inforce, ramp, convin_sense, cap_conv, conv, conv_outforce, conv_sense, prot_conv, " + "conv_en_pull, conv_latch, conv_ovp, comp_pull, buck_rc, boost_rc", + "analog": "reg_analog, reg_vcontrol, reg_vcontroln, reg_vref, ref_div, ref_buf, ref_cap, vcen_rc, " + "dac_ferrite, dac, mv_rc, mi_rc, adc, control, " + "outn, outp, outd", + "mcu": "mcu, led, touch_duck, ioe_ctl, usb_esd, i2c_pull, qwiic_pull, qwiic, dutio", + "sensing": "conv_temp, pass_temp", + "ui": "ioe_ui, enc, dir, rgb, reg_v12, oled, oled_rc, spk_drv, spk", + "tp": "tp_vusb, tp_gnd, tp_3v3, tp_v5, tp_v12, tp_conv, tp_analog, tp_vcontrol, tp_vcontroln, tp_vref, tp_lsrc, tp_lsnk, " + "i2c_tp", + "rf_tp": "tp_vcen, tp_cv, tp_cvf, tp_cisrc, tp_cisnk, tp_mv, tp_mi", + "packed_amps": "vimeas_amps, ampdmeas_amps, cv_amps, ci_amps, cintref_amps", + "misc": "fan_drv, fan, jlc_th", + } + ) + + @override + def multipack(self) -> None: + self.vimeas_amps = self.PackedBlock(Opa2189()) # low noise opamp + self.pack(self.vimeas_amps.elements.request("0"), ["control", "amp", "amp"]) + self.pack(self.vimeas_amps.elements.request("1"), ["control", "hvbuf", "amp"]) + + self.cv_amps = self.PackedBlock(Tlv9152()) + self.pack(self.cv_amps.elements.request("0"), ["ref_buf", "amp"]) # place the reference more centrally + self.pack(self.cv_amps.elements.request("1"), ["control", "err_volt", "amp"]) + + self.ci_amps = self.PackedBlock(Tlv9152()) + self.pack(self.ci_amps.elements.request("0"), ["control", "err_sink", "amp"]) + self.pack(self.ci_amps.elements.request("1"), ["control", "err_source", "amp"]) + + self.cintref_amps = self.PackedBlock(Tlv9152()) + self.pack(self.cintref_amps.elements.request("0"), ["control", "int", "amp"]) + self.pack(self.cintref_amps.elements.request("1"), ["control", "dmeas", "amp"]) # this path matters much less + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["mcu"], Esp32s3_Wroom_1), + (["reg_v5"], Tps54202h), + (["reg_3v3"], Tps54202h), + (["reg_v12"], Lm2733), + (["reg_analog"], Ap2210), + (["reg_vref"], Ref30xx), + (["reg_vcontrol"], Lm2733), + (["control", "driver", "low_fet"], CustomFet), + (["control", "driver", "high_fet"], CustomFet), + (["control", "off_sw", "device"], Sn74lvc1g3157), + (["cap_conv", "cap"], JlcAluminumCapacitor), + (["control", "driver", "cap_in1", "cap"], JlcAluminumCapacitor), + (["spk", "conn"], JstPhKVertical), + (["control", "isense", "ranges[0]", "pwr_sw", "ic"], Tlp3545a), # higher current on 3A range + (["control", "driver", "res"], SeriesResistor), # needed for high power within a basic part + (["oled", "device", "conn"], Fpc050BottomFlip), # more compact connector, double-fold the FPC ribbon + ], + class_refinements=[ + (EspProgrammingHeader, EspProgrammingTc2030), + (TagConnect, TagConnectNonLegged), # really for initial flash / emergency upload only + (Opamp, Tlv9061), # higher precision opamps + (AnalogSwitch, Dg468), + (SolidStateRelay, Tlp170am), + (BananaSafetyJack, Ct3151), + (HalfBridgeDriver, Ncp3420), + (DirectionSwitch, Skrh), + (TestPoint, CompactKeystone5015), + (RotaryEncoder, Pec11s), + (Neopixel, Ws2812c_2020), + (RfConnector, UflConnector), + (UsbEsdDiode, Pgb102st23), # in stock + ], + instance_values=[ + (["mcu", "programming"], "uart-auto"), + ( + ["mcu", "pin_assigns"], + [ + # left side + "speaker.sd=4", + "speaker.sck=5", + "speaker.ws=6", + "int_i2c.sda=7", + "int_i2c.scl=8", + "pd_int=9", + "buck_pwm=10", + "boost_pwm=11", + "vconv_sense=12", + # bottom side + "adc_cs=15", + "adc_spi.sck=17", + "adc_spi.mosi=18", + "adc_spi.miso=19", + "adc_clk=20", + "touch_duck=21", + "limit_source=22", + "limit_sink=23", + "dut0=24", + "dut1=25", + # right side + "rgb=31", + "fan=32", + "enc_sw=33", + "enc_b=34", + "enc_a=35", + "qwiic.sda=38", + "qwiic.scl=39", + "led=_GPIO0_STRAP", + ], + ), + ( + ["ioe_ui", "pin_assigns"], + [ + "dir_a=4", + "dir_cen=5", + "dir_c=6", + "dir_d=7", + "dir_b=12", + "irange_1=9", + "irange_2=10", + "irange_0=11", + ], + ), + ( + ["ioe_ctl", "pin_assigns"], + [ + "ramp=4", + "conv_en=6", + "conv_en_sense=7", + "low_gate=12", + "high_gate=11", + "off_0=9", + ], + ), + # allow the regulator to go into tracking mode + (["reg_v5", "power_path", "dutycycle_limit"], Range(0, float("inf"))), + ( + ["reg_v5", "power_path", "inductor_current_ripple"], + Range(0.01, 0.5), + ), # trade higher Imax for lower L + # JLC does not have frequency specs, must be checked TODO + (["reg_3v3", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + (["reg_v5", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + (["reg_v12", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + (["conv", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + (["reg_vcontrol", "power_path", "inductor", "manual_frequency_rating"], Range.all()), + (["vusb_sense", "Rs", "res", "res", "footprint_spec"], "Resistor_SMD:R_1206_3216Metric"), + ( + ["convin_sense", "Rs", "res", "res", "footprint_spec"], + ParamValue(["vusb_sense", "Rs", "res", "res", "footprint_spec"]), + ), + (["ramp", "drv", "footprint_spec"], "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic"), + # less aggressive derating for smaller part + (["control", "driver", "cap_in1", "cap", "voltage_rating_derating"], 0.8), + # ignore derating on 20v - it's really broken =( + (["reg_v5", "power_path", "in_cap", "cap", "exact_capacitance"], False), + (["reg_v5", "power_path", "in_cap", "cap", "voltage_rating_derating"], 0.85), + (["reg_v12", "cf", "voltage_rating_derating"], 0.85), + (["conv", "power_path", "in_cap", "cap", "exact_capacitance"], False), + (["conv", "power_path", "in_cap", "cap", "voltage_rating_derating"], 0.85), + (["conv", "power_path", "out_cap", "cap", "exact_capacitance"], False), + (["conv", "power_path", "out_cap", "cap", "voltage_rating_derating"], 0.9), # allow using a 35V cap + (["conv", "power_path", "out_cap", "cap", "require_basic_part"], False), + (["control", "driver", "cap_in2", "cap", "voltage_rating_derating"], 0.9), + (["control", "driver", "cap_in3", "cap", "voltage_rating_derating"], 0.9), + (["reg_vcontrol", "cf", "voltage_rating_derating"], 0.85), + (["reg_vcontrol", "power_path", "out_cap", "cap", "exact_capacitance"], False), + (["reg_vcontrol", "power_path", "out_cap", "cap", "voltage_rating_derating"], 0.85), + ( + ["conv", "boost_sw", "high_fet", "gate_voltage"], + ParamValue(["conv", "boost_sw", "low_fet", "gate_voltage"]), + ), # TODO model is broken for unknown reasons + (["boot", "c_fly_pos", "voltage_rating_derating"], 0.85), + (["boot", "c_fly_neg", "voltage_rating_derating"], 0.85), + (["conv", "boost_sw", "low_fet", "footprint_spec"], "Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic"), + (["conv", "boost_sw", "low_fet", "part"], "BSC093N04LSG"), # lower total power + # require all FETs to be the same; note boost must elaborate first + (["conv", "buck_sw", "low_fet", "part"], ParamValue(["conv", "boost_sw", "low_fet", "actual_part"])), + (["conv", "buck_sw", "high_fet", "part"], ParamValue(["conv", "boost_sw", "low_fet", "actual_part"])), + (["conv", "boost_sw", "high_fet", "part"], ParamValue(["conv", "boost_sw", "low_fet", "actual_part"])), + (["conv", "boost_sw", "gate_res"], Range.from_tolerance(4.7, 0.05)), + (["conv", "buck_sw", "gate_res"], ParamValue(["conv", "boost_sw", "gate_res"])), + (["control", "int_link", "sink_impedance"], RangeExpr.INF), # waive impedance check for integrator in + (["control", "isense", "ranges[0]", "isense", "res", "res", "require_basic_part"], False), + (["control", "isense", "ranges[1]", "isense", "res", "res", "require_basic_part"], False), + (["control", "isense", "ranges[2]", "isense", "res", "res", "require_basic_part"], False), + (["control", "driver", "res", "count"], 3), + (["control", "driver", "high_fet", "footprint_spec"], "Package_TO_SOT_THT:TO-220-3_Horizontal_TabUp"), + (["control", "driver", "high_fet", "part_spec"], "IRF540N"), + (["control", "driver", "low_fet", "footprint_spec"], "Package_TO_SOT_THT:TO-220-3_Horizontal_TabUp"), + (["control", "driver", "low_fet", "part_spec"], "IRF9540"), # has a 30V/4A SOA + (["prot_vusb", "diode", "footprint_spec"], "Diode_SMD:D_SMA"), + (["prot_conv", "diode", "footprint_spec"], "Diode_SMD:D_SMA"), + (["prot_3v3", "diode", "footprint_spec"], "Diode_SMD:D_SMA"), + # reduce maximum SSR drive current to be within the IO expander limit + (["control", "isense", "ranges[0]", "pwr_sw", "ic", "led_current_recommendation"], Range(0.002, 0.010)), + (["control", "isense", "ranges[1]", "pwr_sw", "ic", "led_current_recommendation"], Range(0.002, 0.010)), + (["control", "isense", "ranges[2]", "pwr_sw", "ic", "led_current_recommendation"], Range(0.002, 0.010)), + (["spk_drv", "pwr", "current_draw"], Range(6.0e-7, 0.25)), # assume speakers will be pretty mild + # the basic parts in the 100kOhm range is pretty limited + (["ramp", "div", "bottom_res", "require_basic_part"], False), + (["reg_v5", "fb", "div", "top_res", "require_basic_part"], False), + (["reg_v12", "fb", "div", "top_res", "require_basic_part"], False), + (["reg_vcontrol", "fb", "div", "top_res", "require_basic_part"], False), + (["reg_vcontrol", "fb", "div", "bottom_res", "require_basic_part"], False), + # use more basic parts + (["control", "snub_c", "cap", "voltage_rating_derating"], 0.60), # allow use of a 50V basic part caps + (["control", "ifilt", "c", "voltage_rating_derating"], 0.60), + # note, can't limit 5v reg and 12v reg feedback series, no overlap w/ downstream part supply voltages + (["ramp", "div", "series"], 6), + (["oled", "iref_res", "resistance"], Range.from_tolerance(1e6, 0.05)), # use 1M resistor, up from 910k + (["ramp", "drv", "gate_voltage"], Range(0.0, 10.0)), # gate max isn't parsed, but typically up to 20v + # reduce line items + (["control", "err_source", "diode", "part"], ParamValue(["reg_v12", "rect", "actual_part"])), + (["control", "err_sink", "diode", "part"], ParamValue(["reg_v12", "rect", "actual_part"])), + (["reg_vcontrol", "rect", "part"], ParamValue(["reg_v12", "rect", "actual_part"])), + (["conv", "buck_sw", "driver", "boot", "part"], ParamValue(["reg_v12", "rect", "actual_part"])), + (["conv", "boost_sw", "driver", "boot", "part"], ParamValue(["reg_v12", "rect", "actual_part"])), + (["convin_sense", "Rs", "res", "res", "part"], ParamValue(["vusb_sense", "Rs", "res", "res", "part"])), + (["vusb_sense", "Rs", "res", "res", "require_basic_part"], False), + (["convin_sense", "Rs", "res", "res", "require_basic_part"], False), + ( + ["reg_3v3", "power_path", "inductor", "part"], + ParamValue(["reg_v5", "power_path", "inductor", "actual_part"]), + ), + ( + ["reg_vcontrol", "power_path", "inductor", "part"], + ParamValue(["reg_v12", "power_path", "inductor", "actual_part"]), + ), + (["filt_vcontroln", "fb", "part"], ParamValue(["dac_ferrite", "fb", "actual_part"])), + # the low-leakage TVS diodes aren't in the parts table, so directly insert the LCSC part + (["prot_vusb", "diode", "lcsc_part"], ParamValue(["control", "tvs_p", "lcsc_part"])), + (["prot_conv", "diode", "lcsc_part"], ParamValue(["control", "tvs_p", "lcsc_part"])), + ( + ["prot_3v3", "diode", "lcsc_part"], + ParamValue(["control", "tvs_n", "lcsc_part"]), + ), # note, 5v zener diode + # out of stock / unassembleable parts + (["conv", "power_path", "out_cap", "cap", "part"], "C3216X5R1V226MTJ00E"), + ], + class_values=[ + (CompactKeystone5015, ["lcsc_part"], "C5199798"), + (Mcp3561, ["ic", "ch", "0", "impedance"], Range(260e3, 510e3)), # GAIN=1 or lower + (Mcp3561, ["ic", "ch", "1", "impedance"], Range(260e3, 510e3)), # GAIN=1 or lower + (Mcp3561, ["ic", "ch", "2", "impedance"], Range(260e3, 510e3)), # GAIN=1 or lower + (Mcp3561, ["ic", "ch", "3", "impedance"], Range(260e3, 510e3)), # GAIN=1 or lower + (Ws2812c_2020, ["device", "lcsc_part"], "C3646929"), # similar device suitable for economic assembly + ], + ) class UsbSourceMeasureTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(UsbSourceMeasure) + def test_design(self) -> None: + compile_board_inplace(UsbSourceMeasure) diff --git a/examples/test_usb_uart.py b/examples/test_usb_uart.py index bf9dae16b..5657fe0c1 100644 --- a/examples/test_usb_uart.py +++ b/examples/test_usb_uart.py @@ -6,76 +6,74 @@ class UartConnector(Connector, Block): - """UART connector, follows the TXD, RXD, GND, +5 pinning of cheap CP2102 dongles.""" - def __init__(self, *, pwr_current_draw: RangeLike = (0, 0)*mAmp): - super().__init__() - self.conn = self.Block(PassiveConnector()) - - self.uart = self.Port(UartPort.empty(), [InOut]) - # note that RX and TX here are from the connected device, so they're flipped from the CP2102's view - self.connect(self.uart.rx, self.conn.pins.request('1').adapt_to(DigitalSink())) - self.connect(self.uart.tx, self.conn.pins.request('2').adapt_to(DigitalSource())) - self.gnd = self.Export(self.conn.pins.request('3').adapt_to(Ground()), - [Common]) - self.pwr = self.Export(self.conn.pins.request('4').adapt_to(VoltageSink( - current_draw=pwr_current_draw - )), [Power]) + """UART connector, follows the TXD, RXD, GND, +5 pinning of cheap CP2102 dongles.""" + + def __init__(self, *, pwr_current_draw: RangeLike = (0, 0) * mAmp): + super().__init__() + self.conn = self.Block(PassiveConnector()) + + self.uart = self.Port(UartPort.empty(), [InOut]) + # note that RX and TX here are from the connected device, so they're flipped from the CP2102's view + self.connect(self.uart.rx, self.conn.pins.request("1").adapt_to(DigitalSink())) + self.connect(self.uart.tx, self.conn.pins.request("2").adapt_to(DigitalSource())) + self.gnd = self.Export(self.conn.pins.request("3").adapt_to(Ground()), [Common]) + self.pwr = self.Export( + self.conn.pins.request("4").adapt_to(VoltageSink(current_draw=pwr_current_draw)), [Power] + ) class UsbUart(JlcBoardTop): - """USB UART converter board""" - @override - def contents(self) -> None: - super().contents() - self.usb_uart = self.Block(UsbCReceptacle()) - - self.vusb = self.connect(self.usb_uart.pwr) - self.gnd = self.connect(self.usb_uart.gnd) - - # 5v DOMAIN - with self.implicit_connect( - ImplicitConnect(self.vusb, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - # since USB is 5.25 max, we can't use the 5.2v Zener that is a basic part =( - self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6)*Volt)) - - self.usbconv = imp.Block(Cp2102()) - (self.usb_esd, ), self.usb_chain = self.chain( - self.usb_uart.usb, imp.Block(UsbEsdDiode()), self.usbconv.usb) - (self.led, ), _ = self.chain( - self.usbconv.nsuspend, imp.Block(IndicatorLed(Led.White))) - - # for target power only - self.reg_3v3 = imp.Block(LinearRegulator(output_voltage=3.3*Volt(tol=0.05))) - self.v3v3 = self.connect(self.reg_3v3.pwr_out) - - # 3v3 DOMAIN - with self.implicit_connect( - ImplicitConnect(self.v3v3, [Power]), - ImplicitConnect(self.gnd, [Common]), - ) as imp: - self.out = imp.Block(UartConnector()) - self.connect(self.usbconv.uart, self.out.uart) - - @override - def refinements(self) -> Refinements: - return super().refinements() + Refinements( - instance_refinements=[ - (['out', 'conn'], PinHeader254), - (['reg_3v3'], Ap2204k), - ], - instance_values=[ - (['refdes_prefix'], 'U'), # unique refdes for panelization - ], - class_refinements=[ - (UsbEsdDiode, Pgb102st23), # for common parts with the rest of the panel - ], - class_values=[ - ], - ) + """USB UART converter board""" + + @override + def contents(self) -> None: + super().contents() + self.usb_uart = self.Block(UsbCReceptacle()) + + self.vusb = self.connect(self.usb_uart.pwr) + self.gnd = self.connect(self.usb_uart.gnd) + + # 5v DOMAIN + with self.implicit_connect( + ImplicitConnect(self.vusb, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + # since USB is 5.25 max, we can't use the 5.2v Zener that is a basic part =( + self.vusb_protect = imp.Block(ProtectionZenerDiode(voltage=(5.25, 6) * Volt)) + + self.usbconv = imp.Block(Cp2102()) + (self.usb_esd,), self.usb_chain = self.chain(self.usb_uart.usb, imp.Block(UsbEsdDiode()), self.usbconv.usb) + (self.led,), _ = self.chain(self.usbconv.nsuspend, imp.Block(IndicatorLed(Led.White))) + + # for target power only + self.reg_3v3 = imp.Block(LinearRegulator(output_voltage=3.3 * Volt(tol=0.05))) + self.v3v3 = self.connect(self.reg_3v3.pwr_out) + + # 3v3 DOMAIN + with self.implicit_connect( + ImplicitConnect(self.v3v3, [Power]), + ImplicitConnect(self.gnd, [Common]), + ) as imp: + self.out = imp.Block(UartConnector()) + self.connect(self.usbconv.uart, self.out.uart) + + @override + def refinements(self) -> Refinements: + return super().refinements() + Refinements( + instance_refinements=[ + (["out", "conn"], PinHeader254), + (["reg_3v3"], Ap2204k), + ], + instance_values=[ + (["refdes_prefix"], "U"), # unique refdes for panelization + ], + class_refinements=[ + (UsbEsdDiode, Pgb102st23), # for common parts with the rest of the panel + ], + class_values=[], + ) class UsbUartTestCase(unittest.TestCase): - def test_design(self) -> None: - compile_board_inplace(UsbUart) + def test_design(self) -> None: + compile_board_inplace(UsbUart) diff --git a/perf.py b/perf.py index d033d5db3..80b7dd85a 100644 --- a/perf.py +++ b/perf.py @@ -4,14 +4,14 @@ from edg import compile_board_inplace from examples.test_datalogger import Datalogger -if __name__ == '__main__': - pr = cProfile.Profile() - pr.enable() - compile_board_inplace(Datalogger) - pr.disable() +if __name__ == "__main__": + pr = cProfile.Profile() + pr.enable() + compile_board_inplace(Datalogger) + pr.disable() - s = io.StringIO() - sortby = SortKey.CUMULATIVE - ps = pstats.Stats(pr, stream=s).sort_stats(sortby) - ps.print_stats() - print(s.getvalue()) + s = io.StringIO() + sortby = SortKey.CUMULATIVE + ps = pstats.Stats(pr, stream=s).sort_stats(sortby) + ps.print_stats() + print(s.getvalue()) diff --git a/pyproject.toml b/pyproject.toml index a2cd92e1e..67eaf4c5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,11 @@ dependencies = [ ] requires-python = ">=3.9" +[project.optional-dependencies] +dev = [ + "black", +] + [tool.setuptools] packages = ["edg.edgir", "edg.edgrpc", "edg.core", "edg.hdl_server", "edg.electronics_model", "edg.abstract_parts", "edg.parts", "edg.jlcparts", "edg"] @@ -40,5 +45,8 @@ implicit_reexport = true module = ["edg.edgir.*", "edg.edgrpc.*"] # generated code ignore_errors = true +[tool.black] +line-length = 120 + [project.urls] Homepage = "https://github.com/BerkeleyHCI/PolymorphicBlocks"