diff --git a/getting_started_schimport.md b/getting_started_schimport.md
index 7ad0b1626..61056c08e 100644
--- a/getting_started_schimport.md
+++ b/getting_started_schimport.md
@@ -209,37 +209,9 @@ However, instead of using `auto_adapt`, we can instead define `conversions` on a
> ```
-## Reference
-
-These common symbols can be used in schematic import and map to the following passive-typed HDL blocks:
-
-| Symbol | HDL Block | Value Parsing | Notes |
-|------------------------------------|--------------------|-----------------|----------------------------------|
-| Device:C, Device:C_Polarized | Capacitor | e.g. `10uF 10V` | Voltage rating must be specified |
-| Device:R | Resistor | e.g. `100` | |
-| Device:L | Inductor | | |
-| Device:Q_NPN_\*, Device:Q_PNP_\* | Bjt.Npn, Bjt.Pnp | | |
-| Device:D | Diode | | |
-| Device:D_Zener | ZenerDiode | | |
-| Device:L_Ferrite | FerriteBead | | |
-| Device:Q_NMOS_\*, Device:Q_PMOS_\* | Fet.NFet, Fet.PFet | | |
-| Switch:SW_SPST | Switch | | |
-
-Notes:
-- Blocks are Passive-typed unless otherwise noted.
-- If Value Parsing is empty, the Block can only be defined by HDL instantiation or inline HDL.
-- All Blocks that can be used in schematic import can be found by searching for all subclasses of `KiCadImportableBlock`.
-- In many cases, the `_Small` (like `Device:C_Small`) symbol can also be used.
-
-These higher-level symbols have typed pins (like VoltageSink, Ground, and AnalogSink) and can be used to make higher-level analog signal chains:
-
-| Symbol | HDL Block | Notes |
-|--------------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------|
-| Simulation_SPICE:OPAMP | Opamp | Supports value parsing (with empty value); is the full application circuit for an opamp (including decoupling capacitors) |
-| edg_importable:Amplifier | Amplifier | |
-| edg_importable:DifferentialAmplifier | DifferentialAmplifier | |
-| edg_importable:IntegratorInverting | IntegratorInverting | |
-| edg_importable:OpampCurrentSensor | OpampCurrentSensor | |
+## References
+
+See the [library construction reference](reference_library.md) for a list of KiCad symbols.
## Defining Library Parts
diff --git a/reference_library.md b/reference_library.md
index ff018df3f..ce1f7a188 100644
--- a/reference_library.md
+++ b/reference_library.md
@@ -6,222 +6,386 @@ New? Consider reading through the [getting started guide](getting-started.md), i
Also see the [top-level board design reference](reference.md).
+This document provides a quick reference and list of common primitives and design patterns.
+However, your best resource is going to be all the examples in the parts libraries.
## Dragons be Ahead
The library design experience is much less polished and more intricate than the top-level board design experience.
-## Under Construction
-Parts of this reference are outdated.
+## Blocks
+`Block`s are a subcircuit that contain boundary ports, optional parameters, internal sub-blocks, and connections between them.
-## Parameters
-- `BoolExpr`: boolean (true / false) variable
- - Supports standard boolean operations
-- `FloatExpr`: numeric variable
- - Supports standard numeric operations
-- `RangeExpr`: range (interval) variable
- - Supports standard numeric operations (multiplication and division are undefined), some set operations (`.within(other)`, `.contains(other)`, `.intersect(other)`), and get operations (`.lower()`, `.upper()`)
-- `StringExpr`: string (text) variable
- - Supports equality operations only
+```python
+class IndicatorLed(Block):
+ def __init__(self, current_draw: RangeLike = (1, 10) * mAmp) -> None:
+ super().__init__()
+ self.current_draw = self.ArgParameter(current_draw)
+ self.actual_current_draw = self.Parameter(RangeExpr())
+ self.gnd = self.Port(Ground(), [Common])
+ self.signal = self.Port(DigitalSink(...))
+
+ def contents(self) -> None:
+ super().contents()
+ self.led = self.Block(Led(...))
+ self.res = self.Block(Resistor(resistance=self.signal.link().voltage / self.current_draw))
+ self.assign(self.actual_current_draw, self.signal.voltage / self.res.resistance)
+ self.connect(self.signal.net, self.led.a)
+ self.connect(self.res.a, self.led.k)
+ self.connect(self.gnd.net, self.res.b)
+```
-## Blocks
-Blocks represent a subcircuit (or hierarchical schematic sheet), and consist of boundary ports and internal subblocks and connections (links) between ports on those subblocks.
+- The `Block` API for top-level board design is also available in subcircuit `Block`s.
+
+### Port Interfaces
+
+- Boundary ports are defined with `self.Port(PortType(...)` and must be defined in `__init__`.
+ - Boundary ports may optionally have `tags=[...]` to support implicit connections.
+ Common tags are `Power` (v+), `Common` (gnd).
+ Tags to support `chain` are `Input`, `Output`, and `InOut`.
+ - Boundary ports may optionally define `optional=True` to indicate that the port may be left unconnected.
+ - Use `PortType.empty()` to define a port without modeling, where its modeling is defined by the inner port it is connected to.
+- Ports are connected with `self.connect(...)`; these may be part of a connect:
+ - Boundary ports (as a unit) or their bundle inner ports (separately).
+ - Boundary ports of sub-blocks (as a unit only).
+
+>
+> Common Port Types
+>
+> - `Passive`: a single netlist pin without electrical modeling, a building block for ports with electrical modeling on top
+>
+> Single Wire Port that wrap (has-a) `Passive`:
+> - `VoltageSource`, `VoltageSink`: models voltage and current, and their limits.
+> - `DigitalSource`, `DigitalSink`, `DigitalBidir`: models voltage, voltage thresholds, current, and their limits.
+> Checks for multiple-driver conflicts, with multiple `DigitalBidir` drivers are allowed.
+> - `AnalogSource`, `AnalogSink`: models voltage, current, and input/output impedance, and their limits.
+> Arbitrary requirement that the source has 1/10 the parallel impedance of sinks.
+>
+> Bundle Ports
+> - `I2cController`, `I2cTarget`: models I2C as two digital ports and address.
+> Checks for address conflicts.
+> - `SpiController`, `SpiPeripheral`: models the shared SPI lines (excluding CS) as three digital ports.
+> - `UartPort`: models UART TX/RX as two digital ports.
+> Connections generate a crossover connection.
+> - `UsbHostPort`, `UsbDevicePort`: USB D+/D- ports
+> - `CanControllerPort`, `CanTransceiverPort`: controller-side and transceiver-side RXD and TXD ports.
+> - `CanDiffPort`: differential-side CAN (CANH, CANL) ports
+>
+> See their class docstring and constructor for details.
+>
+> See Design Patterns below for adapting between (some) port types.
+>
+>
+
+### Parameters
+
+Parameters are variables that can be passed into and through blocks.
+
+- Parameters are defined purely symbolically and concrete values are not available to (non-generator) `Block`s.
+- Parameters are restricted to a set of types supported by the compiler and the operations on them:
+ - `BoolExpr` / `BoolLike`: boolean (true / false) variable
+ - `IntExpr` / `IntLike`: numeric variable
+ - `FloatExpr` / `FloatLike`: numeric variable
+ - `RangeExpr` / `RangeLike`: range (interval) variable
+ - `StringExpr` / `StringLike`: string (text) variable
+ - See the `xExpr` class definition for supported operations.
+ - See the generator section for how to use arbitrary Python code for calculations.
+- Parameters must be defined in `__init__`, either with:
+ - `self.ArgParameter(arg)`, to wrap `__init__` argument `arg`.
+ Use the `xLike` type for `__init__` arguments, which also allow the literal Python value.
+ - `self.Parameter(ParameterType())` (for a parameter without a value, to be assigned in `contents()`).
+ Use the `xExpr` type in `Parameter(...)` to declare a parameter without a value, to be assigned in `contents()`.
+- `RangeExpr` types are used to represent a device tolerance or toleranced specification
+ - Most operations between `RangeExpr` types are tolerance-expanding (computes the worst-case range of the inputs)
+ - `a = c.shrink_multiply(b)` is a tolerance-shrinking operation to calculate the maximum allowable toleranced `a`, such that tolerance-expanding `a * b` is equal to `c`.
+ Example: for `v = i * r`, to solve for allowable (target) `r` given requirement `i` and contributing tolerance `v`, use `r = (1/i).shrink_multiply(v)`
+- Access parameters on the link of ports using `port.link().link_param`.
+ Links contain the aggregated parameter of all connected ports, for example the voltage.
+
+Quirks:
+- `port.link().link_param` will be undefined for an unconnected port.
+ For optional ports, gate checks with `port.is_connected().else_then(..., ...)`.
+ This may be fixed eventually, see [#360](https://github.com/BerkeleyHCI/PolymorphicBlocks/issues/360).
+
+### Footprints
+
+The `FootprintBlock` base class is a `Block` that defines at most one footprint.
-Skeleton structure:
```python
-class MyBlock(Block):
- def __init__(self) -> None:
- super().__init__() # essential to call the superclass method beforehand to initialize state
- # declare ports here, and subblocks whose ports are exported
-
- def contents(self) -> None:
- super().contents() # essential to call the superclass method beforehand to initialize state
- # declare subblocks and connections here
+class Sk6812Mini_E(FootprintBlock):
+ def __init__(self) -> None:
+ super().__init__()
+ self.gnd = self.Port(Ground())
+ self.vdd = self.Port(VoltageSink(...))
+ self.din = self.Port(DigitalSink(...))
+ self.dout = self.Port(DigitalSource(...))
+
+ 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",
+ )
```
-These are properties of Blocks:
-- The Block type hierarchy defines allowed refinements
-- `@abstract_block` decorates a block as abstract, which will error on netlisting
-
-These can be called inside `__init__(...)`:
-- `self.Parameter(ParameterType(optional_value))`: declare parameters, optionally with a value
-- `self.Port(PortType(...))`: declare ports
- - Optional arguments: `tags=[ImplicitTags], optional=False`
- - `self.Export(self.subblock.port)`: export a subblock's port
-- `@init_in_parent` decorator is needed if `__init__(...)` takes Parameter arguments and must generate constraints in the parent Block
-
-These can be called inside `contents()`:
-- `self.Block(BlockType(...))`: declare sub-blocks
- - Also allowed inside `__init__(...)`, to allow the use of `Export`s
-- `self.constrain(...)`: constrain some `BoolExpr` (which can be an inline expression, eg `self.param1 == self.param2`) to be true
-- `self.connect(self.subblock1.port, self.subblock2.port, ...)`: connect own ports and/or subblock's ports
- - Naming is optional.
-- `with self.implicit_connect(...) as imp:`: open an implicit connection scope
- - Implicit connection arguments of the form `ImplicitConnect(connect_to_port, [MatchingTags])`
- - `imp.Block(BlockType(...))`: similar to `self.Block(...)` but implicitly connects ports with matching tags
-- `self.chain(self.Block(BlockType(...)), self.Block(BlockType(...)), ...)`: chain-connect between blocks, in `contents`
- - Return of `self.chain` can be unpacked into a tuple of chained blocks, and the chain object (itself).
- Naming of the chain object is optional.
- - Elements are chained from left (outputs) to right (inputs)
- - The first argument to chain may be a port or block with an `InOut` or `Output`-tagged port.
- - Middle elements must be a block with an `InOut`-tagged port, or `Input`- and `Output`-tagged ports.
- - The last argument to chain may be a port or block with an `InOut` or `Input`-tagged port.
-- Assign names to components by assigning the object to a `self` variable:
- - `self.subblock_name = self.Block(BlockType(...))`
- - `(self.subblock_name1, self.subblock_name2, ...), self.chain_name = self.chain(...)`
-
-### Generators
-Generators allow some Python code to run that has access to the solved values of some parameters.
-TODO - write this section pending generator refactoring, in the meantime see the port array section of the getting started tutorial.
-
-
-### Footprint
-FootprintBlock is a block that is associated with a PCB footprint.
-
-All primitives that can be called inside `Block`'s `__init__(...)` can be called inside `__init__(...)`
-
-These can be called inside `contents()`:
-- `self.footprint(refdes='R', footprint='Resistor_SMD:R_0603_1608Metric', pinning={...})`: associates a footprint with this block
- - Pinning argument format: `{'pin_name': self.port, ...}`: associates footprint pins with ports or subports
- - Optional arguments: `mfr='Manufacturer A', part='Part Number', value='1kOhm', datasheet='www.example.net'`
-
-
-## Port and Link Libraries
-
-### Single-wire Ports
-- `VoltageLink`: voltage rail
- - `VoltageSource(voltage, current_limits)`: voltage source
- - `VoltageSink(voltage_limits, current_draw)`: voltage sink (power input)
-- `DigitalLink`: low-speed (up to ~100 MHz) digital signals, modeling voltage limits and input / output thresholds
- - `DigitalSource(voltage, current_limits, output_thresholds)`: digital output-only pin
- - `output_thresholds` is the range of (maximum low output, minimum high output)
- - `DigitalSink(voltage_limits, current_draw, input_thresholds)`: digital input-only pin
- - `input_thresholds` is the range of (maximum low input, minimum high input)
- - `DigitalBidir(...)`: digital bidirectional (eg, GPIO) pin
- - Has all arguments of `DigitalSource` and `DigitalSink`
-- `AnalogLink`: analog signal that models input and output impedance
- - `AnalogSource(voltage, current_limits, impedance)`: analog output
- - `AnalogSink(voltage_limits, current_draw, impedance)`: analog signal input
-- `PassiveLink`: connected copper that contains no additional data
- - `Passive`: single wire port that contains no additional data, but can be adapted to other types (eg, with `.as_voltage_source(...)`).
- Useful for low-level elements (eg, resistors) that can be used in several ways in higher-level constructs (eg, pull-up resistor for digital applications).
-
-### Bundle Ports
-- `I2cLink`: I2C net, consisting of `.scl` and `.sda` Digital sub-ports.
- - `I2CMaster(model)`: I2C master port
- - `model`: DigitalBidir model for both SCL and SDA sub-ports
- - `I2CSlave(model)`: I2C slave port
-- `SpiLink`: SPI net, consisting of `.sck`, `.miso`, and `.mosi` Digital sub-ports. CS must be handled separately.
- - `SpiMaster(model, frequency)`: SPI master port
- - `frequency`: range of allowable frequencies, generally including zero
- - `SpiSlave(model, frequency)`: SPI slave port
-- `UartLink`: UART net, consisting of `.tx` and `.rx` Digital sub-ports in a crossover point-to-point connection.
- - `UartPort(model)`: UART port
-- `UsbLink`: USB data net, consisting of `.dp` (D+) and `.dm` (D-) Digital sub-ports in a point-to-point connection.
- - `UsbHostPort`: USB host
- - `UsbDevicePort`: USB device
- - `UsbPassivePort`: other components (eg, TVS diode) on the USB line
-- `CanLogicLink`: CAN logic-level (TXD/RXD) net, consisting of `.txd` and `.rxd` Digital sub-ports in a point-to-point connection.
- - `CanControllerPort(model)`: CAN controller-side port
- - `CanTransceiverPort(model)`: CAN transceiver-side port
-- `CanDiffLink`: CAN differential (CANH/CANL) net, consisting of `.canh` and `.canl` Digital sub-ports in a bus connection.
- - `CanDiffPort(model)`: CAN node port
-- `SwdLink`: SWD programming net, consisting of `.swdio`, `.swclk`, `.swo`, and `.reset` Digital sub-ports in a point-to-point connection.
- - `SwdHostPort(model)`: SWD host-side (programmer) port
- - `SwdTargetPort(model)`: SWD target-side (DUT) port
-- `CrystalLink`: crystal net, consisting of `.xi` and `.xo` sub-ports in a point-to-point connection.
- - `CrystalDriver(frequency_limits, voltage_out)`: driver-side port
- - `CrystalPort(frequency)`: crystal-side port, indicating the frequency of the crystal
-
-
-## Block Libraries
-The IDE's library tab provides a categorized list of available library blocks.
-Many blocks also have a short descriptive docstring.
-
-
-## Advanced Core Primitives
-These are core primitives needed to model new ports and links
-
-### Ports
-Ports can have parameters and may be connected to each other via a Link.
-
-Skeleton structure:
+- `self.footprint(...)` defines the footprint associated with this block and its pinning and takes these arguments:
+ - `refdes`
+ - `footprint`: KiCad footprint name.
+ - `pinning`: dictionary mapping footprint pin numbers to ports.
+ - Pin numbers must be `str` or `Tuple[str, ...]` (for multi-pin pads).
+ - Ports must be `Passive` or `HasPassive`.
+ - Optional metadata `mfr`, `part`, `value`, `datasheet`.
+ - Optional pick-and-place metadata `pnp_rot`, `pnp_offset` for KiCad footprint to JLC PCBA PnP data.
+- While not (yet) forbidden, `FootprintBlock`s should not have inner sub-`Block`s.
+
+## Schematic-Defined Blocks
+
+`KiCadSchematicBlock` is a `Block` that is defined by a KiCad schematic sheet.
+
+```python
+class MySchematicDefinedBlock(KiCadSchematicBlock):
+ def __init__(self) -> None:
+ super().__init__()
+ self.gnd = self.Port(Ground())
+ self.pwr = self.Port(VoltageSink(...))
+
+ def contents(self) -> None:
+ super().contents()
+ self.import_kicad(
+ self.file_path(f"{self.__class__.__name__}.kicad_sch"),
+ conversions={'pwr': VoltageSink(...), 'gnd': Ground()},
+ auto_adapt=True
+ )
+```
+
+- The HDL stub is required to define the boundary ports and parameters.
+- Graphical schematic components map as follows:
+ - Labels, including symbols like GND and VCC, connect internally.
+ - Hierarchical labels connect to the boundary ports.
+ - True global labels (that connect design-wide) are not supported.
+ - Components map to `Block`s, with the refdes mapping to the `Block` name.
+ - Wires map to `connect`s.
+ - Pins must be connected at a wire end or bend.
+- Components can be defined as:
+ - Using a `KiCadInstantiableBlock` (which defines the symbol to port mapping), and the symbol has no footprint:
+ - **Value parsing**: some blocks can parse the symbol value to parameters, see the table below.
+ - **HDL instantiation**: the block is instantiated in HDL and the symbol refdes matches the HDL `Block` name and has no value.
+ - **Inline HDL**: the symbol value starts with a `#` and contains Python code to instantiate the `Block`.
+ Multi-line values supported using the `Value2`, `Value3`, ... fields.
+ - **Blackboxing**, where the symbol has a footprint specified and is created as a `Block` with all `Passive` ports.
+- Port connections are direct and types of connected pins must be compatible.
+ - Use `conversions` to optionally specify a electrically typed `Port` model for a `Passive` `Port`.
+ - Use `auto_adapt` to automatically insert ideal adapters from `Passive` `Port`s to the HDL-defined electrically typed boundary ports.
+
+>
+> Common `KiCadInstantiableBlock`s
+>
+> With Passive-typed Ports, typically for constructing basic (sub)circuits:
+>
+> | Symbol | HDL Block | Value Parsing |
+> |------------------------------------|--------------------|------------------|
+> | Device:C, Device:C_Polarized | Capacitor | e.g. `10uF 10V`* |
+> | Device:R | Resistor | e.g. `100` |
+> | Device:L | Inductor | |
+> | Device:Q_NPN_\*, Device:Q_PNP_\* | Bjt.Npn, Bjt.Pnp | |
+> | Device:D | Diode | |
+> | Device:D_Zener | ZenerDiode | |
+> | Device:L_Ferrite | FerriteBead | |
+> | Device:Q_NMOS_\*, Device:Q_PMOS_\* | Fet.NFet, Fet.PFet | |
+> | Switch:SW_SPST | Switch | |
+> * Capacitor voltage is required and is specified as the expected operating voltage, not a rating.
+>
+> Where value parsing is empty, the Block can only be defined by HDL instantiation or inline HDL.
+>
+> In many cases, the `_Small` (like `Device:C_Small`) symbol can also be used.
+>
+> With electrically-typed ports, typically used in higher-level subcircuits like analog signal-processing chains:
+>
+> | Symbol | HDL Block |
+> |--------------------------------------|-----------------------|
+> | Simulation_SPICE:OPAMP | Opamp* |
+> | edg_importable:Amplifier | Amplifier |
+> | edg_importable:DifferentialAmplifier | DifferentialAmplifier |
+> | edg_importable:IntegratorInverting | IntegratorInverting |
+> | edg_importable:OpampCurrentSensor | OpampCurrentSensor |
+> * `Opamp`s include any datasheet-required supporting circuitry like decoupling capacitors.
+>
+> Search for all subclasses of `KiCadImportableBlock` for a complete listing.
+>
+>
+
+Guidance:
+- Good uses include analog subcircuits where the graphical connectivity is complex and meaningful.
+- This can be used to construct both library subcircuits as well a higher-level subcircuits like signal-processing chains using amplifier subcircuits.
+- Most chip subcircuits are defined purely in HDL.
+
+## Generators
+
+`GeneratorBlock` is a `Block` that can retrieve the concrete value of its parameters and run arbitrary Python code to create its inner definition, including structural circuitry and parameters.
+
```python
-class MyPort(Port[MyPortLinkType]):
- link_type = MyPortLinkType # required
- bridge_type = MyPortBridgeType # optional, if a bridge is needed
-
- def __init__(self) -> None:
- super().__init__() # essential to call the superclass method beforehand to initialize state
- # declare elements like parameters here
+class IndicatorLedArray(GeneratorBlock):
+ def __init__(self, count: IntLike):
+ super().__init__()
+ self.count = self.ArgParameter(count)
+ self.generator_param(self.count)
+
+ self.gnd = self.Port(Ground.empty(), [Common])
+ self.signals = self.Port(Vector(DigitalSink.empty()))
+
+ 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.connect(...)
```
-These are properties of Ports:
-- The Port type hierarchy currently is not used. However, inheritance may still be useful for code re-use / de-duplication.
+- `self.generator_param(...)` is required to declare parameters that the generator needs.
+ - Only `ArgParameter`s, `port.is_connected()` and `port.elements()` are allowed.
+- `self.get(...)` can be invoked in `generate()` to retrieve the concrete value of a parameter.
-These can be called inside `__init__(...)`:
-- `__init__(...)` may take arguments, and no decorators (as with Block) are needed
-- `self.Parameter(ParameterType(optional_value))`: declare parameters, optionally with a value
+Guidance:
+- Generators are necessary for parametric structural circuit construction.
+- Prefer using non-generator expression operations for parameter calculations where possible.
+ Generators should only be used where the expression operations are insufficient.
-### Bundles
-Bundles are a special type of Port that is made up of constituent sub-Ports.
+## Abstract Blocks
+
+`abstract_block`s are `Blocks` that define an interface that can be implemented by (concrete) subclasses.
+They can be instantiated but will error during compilation if not refined.
-Skeleton structure:
```python
-class MyBundle(Bundle):
- link_type = MyBundleLinkType
- bridge_type = MyBundleBridgeType # optional, if a bridge is needed
-
- def __init__(self) -> None:
- super().__init__() # essential to call the superclass method beforehand to initialize state
- # declare elements like parameters here
+@abstract_block
+class Led(Block):
+ def __init__(self):
+ self.a = self.Port(Passive.empty())
+ self.k = self.Port(Passive.empty())
```
-In addition to the primitives that can be called inside `Port`'s `__init__(...)`, these can be called inside `Bundle`'s `__init__(...)`:
-- `self.Port(PortType(...))`: declare bundle sub-port
+- Boundary ports and parameters are defined as usual.
+- Ports are defined with `PortType.empty()` to allow subclasses flexibility in modeling.
+ - Subclasses can specify modeling with `self.port.init_from(PortType(...))`.
+ Replacing the port object is not allowed.
+- A default refinement can be attached to the `abstract_block` with `@abstract_block_default(lambda: DefaultRefinementBlock)`.
+ - The `lambda` is required to break circular definitions.
+ - Top-level designers can still override these refinements.
+
+Guidance:
+- Use `abstract_block`s to define a common interface for `Block`s that are drop-in interchangeable.
+- You can always refactor to pull out a common `abstract_block` later.
+
+### Mixins
-### Links
-Links define propagation rules for connected Ports.
+`BlockInterfaceMixin`s define an interface that can be added to an `abstract_block`.
-Links are structured the same as Blocks, with similar primitives (see the Block documentation for details):
```python
-class MyPortLink(Link):
- def __init__(self) -> None:
- super().__init__() # essential to call the superclass method beforehand to initialize state
- # declare ports here
-
- def contents(self) -> None:
- super().contents() # essential to call the superclass method beforehand to initialize state
- # declare constraints and internal connections here
+# mixin interface definition
+class RotaryEncoderSwitch(BlockInterfaceMixin[RotaryEncoder]):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+ self.sw = self.Port(Passive.empty(), optional=True)
+
+# concrete class implementing the mixin
+class Ec11eWithSwitch(RotaryEncoderSwitch, RotaryEncoder):
+ ...
```
-These are properties of Links:
-- Each Port can have one type of associated link.
-- The Link type hierarchy currently is not used. However, inheritance may still be useful for code re-use / de-duplication.
-- Extend `CircuitLink` instead of `Link` to copper-connect all ports.
-
-These can be called inside `__init__()`:
-- `__init__()` cannot have arguments, since links are always inferred
-- `self.Parameter(ParameterType(optional_value))`
-- `self.Port(PortType(...))` (without tags, but can have optional)
-
-These can be called inside `contents()`:
-- `self.connect(self.port1.subport, self.port2.subport, ...)`: for Bundle ports, connect sub-ports and infer inner link
- - Naming is optional.
-- `self.constrain(...)`
+- A `Block` definition can inherit multiple mixins (the `Block` implements multiple mixins).
+- A `Block` instantiation can use multiple mixins (requiring the concrete `Block` refinement to implement those mixins).
+- Mixin interface ports are typically `optional` to allow usage without the mixin.
+ This may be a generator that provides a default connection in a basic usage configuration.
+
+### Abstract Passives and Parts Tables
+
+Many common passive and discrete parts (like `Resistor`, `Capacitor`, `Diode`) are `abstract_blocks` and support different (including user-defined) implementations, e.g. to support different vendors and distributors.
+
+Many of these include a utility subclass for selecting a part from a table (like `TableResistor`, `TableCapacitor`, `TableDiode`).
+
+These also provide a separate utility subclass that defines KiCad footprint to port mappings that implement the `HasStandardFootprint` interface (like `ResistorStandardFootprint`, `CapacitorStandardFootprint`, `DiodeStandardFootprint`).
+
+These classes provide utility functions for parts table selectors:
+- `PartsTablePart`: interface parameters (part requirement, excluded parts, and more) for part selected from a parts table.
+- `PartsTableSelector`: utility code to select a part from a `PartsTable`, implementing `PartsTableSelector`.
+- `SelectorFootprint`: interface parameters to allow filtering by footprint.
+- `PartsTableFootprintFilter`: utility code to filter a parts table by footprint, implementing `SelectorFootprint`.
+- `PartsTableSelectorFootprint`: utility code to construct the footprint, for a `HasStandardFootprint`.
+
+Look at some example implementations for the `PartsTable` API and utilities.
+
+## Design Patterns and Conventions
+
+These are design patterns and conventions used in the included parts library.
+
+### `_Device` Footprint and Subcircuit
+
+The `x_Device` `Block` defines the footprint only, while the application circuit `Block` is the user-friendly name and instantiates `x_Device`.
+`x_Device` is not exported in `__init__.py`.
+
+### Passive and Typed Layers
+
+Passive components are `Passive` port typed, and applications of them are separate `Block`s that instantiate the `Passive` typed `Block`s.
+This allows different implementation of the same `Passive` device, eg, `PullupResistor`, `PulldownResistor`.
+Subcircuits follow this pattern too, with them instantiating the `Passive` typed `Block`, connecting the `Passive` ports internally and to the `Passive` `.net` of the electrically-typed boundary `Port`s.
+
+### Target and Actual Parameters
+
+Most parameters are defined as a target requirement.
+
+The actual expected operating value may be calculated as a readout parameter and is prefixed `actual_`.
+
+### PassiveConnector
+
+Devices that use a standard connector (like FPCs) should instantiate the appropriate `PassiveConnector` subclass to allow the system designer to make choices for the concrete connector.
+`PassiveConnector` supports parameterized pin counts.
+
+### Circular Dependencies
+
+There is currently no structural prevention of circular parameter dependencies.
+
+This may happen when a `Block` has a parameter that is calculated from a `Port`'s link, and the `Port`'s link is calculated from the `Block`'s parameter.
+For example, the LED calculates current from the link's voltage, while a source block that calculates voltage from the link's current would be unsolvable.
+
+We typically resolve this by using the user-specified range for one of the parameters and accepting that it will be a wider range than the actual operating range.
+
+
+## Custom Ports and Links
+
+Custom `Port`s and `Link`s can be defined.
+
+`Port`s should define properties of that `Port` only, not properties influenced by any other connected `Port`.
+The `Link` aggregates parameters on the connected `Ports` and optionally defines constraints as assertions.
+
+`Port`s can contain other `Port`s, such as:
+- Bundles of `Port`s, like `SpiController`
+- An inner `Passive` that provides the connectivity layer
+
+The design convention for whether bundle `Port`s are `Passive` or electrically typed is:
+- Protocols that define an electrical specification are `Passive` typed.
+ The electrical modeling is functionally redundant and connections between any parts should be compatible.
+ Example: CAN, USB.
+- Protocols that only define the signal specification are electrically typed.
+ Electrical modeling is needed to check for compatibility.
+ Examples: SPI, I2C, which use `DigitalSource` / `DigitalSink` / `DigitalBidir` internally.
+
+Look at examples in the standard interfaces library for details.
### Bridges
-Bridges are a special type of `Block` that adapts from a Link-facing (inner, `self.inner_link`) port to a Block edge (outer, `self.outer_port`) port.
-Extend `CircuitPortBridge` instead of `PortBridge` to copper-connect all ports.
-In `__init__()` (may not take arguments), declare these two required ports.
-In `contents()`, add constraints as needed.
+`PortBridge` are a quirk of the hierarchical model: how to expose a single boundary `Port` of a `Block` that is internally connecting multiple internal `Port`s.
+
+`PortBridge` "aggregate" the `inner_link` (by connecting to a `Port` on that `Link`) and expose a single `outer_port`.
+
+General design patterns:
+- Parameters always propagate outward, never inward.
+- Checks that require context at the outer level are handled by propagating the relevant parameters outward.
+- If needed, define additional internal parameters (`_`-prefixed) on the `Port` to propagate all the relevant data outward.
-### Adapters
-PortAdapters are a special type of `Block` that adapts / converts from a source (`self.src`) port to a destination (`self.dst`) port.
-Extend `CircuitPortAdapter` instead of `PortAdapter` to copper-connect all ports.
+### Mixins
-In `__init__(...)` (may take arguments, must decorate with `@init_in_parent` if arguments), declare these two required ports.
-In `contents()`, add constraints as needed.
+Mixins on `Port`s may be an interesting concept to support opt-in specialized parameter propagation without an excessive base (see [#333](https://github.com/BerkeleyHCI/PolymorphicBlocks/issues/333)) but do not have an implementation plan yet.