mirror of
https://github.com/diodeinc/pcb.git
synced 2026-09-14 20:17:19 +08:00
3e0b15929e
Signed-off-by: akhilles <akhilvelagapudi@gmail.com>
1702 lines
56 KiB
Plaintext
1702 lines
56 KiB
Plaintext
---
|
||
title: "Zener Language"
|
||
description: "Hardware Description Language for PCB design"
|
||
---
|
||
|
||
# Specification
|
||
|
||
## Overview
|
||
|
||
Zener is a domain-specific language built on top of
|
||
[Starlark](https://github.com/bazelbuild/starlark/blob/master/spec.md) for
|
||
describing PCB schematics. It provides primitives for defining components, symbols, nets,
|
||
interfaces, and modules in a type-safe, composable manner.
|
||
|
||
This specification describes the language extensions and primitives added on top
|
||
of Starlark. For the base Starlark language features, please refer to the
|
||
[Starlark specification](https://github.com/bazelbuild/starlark/blob/master/spec.md) and the
|
||
[starlark-rust types extension](https://github.com/facebook/starlark-rust/blob/main/docs/types.md).
|
||
|
||
## Table of Contents
|
||
|
||
1. [Evaluation Model](#evaluation-model)
|
||
2. [Core Types](#core-types)
|
||
3. [Built-in Functions](#built-in-functions)
|
||
4. [Module System](#module-system)
|
||
5. [Type System](#type-system)
|
||
|
||
## Evaluation Model
|
||
|
||
### Files as Modules
|
||
|
||
Each `.zen` file is a Starlark module. It can be used in two ways:
|
||
|
||
1. Its exported symbols can be `load()`ed into other modules. For example,
|
||
`load("./MyFile.zen", "MyFunction", "MyType")` will load the `MyFunction`
|
||
and `MyType` symbols from the `MyFile.zen` module.
|
||
2. It can be loaded as a schematic module using the `Module()` helper. For
|
||
example, `MyFile = Module("./MyFile.zen")` will import `MyFile.zen` as a
|
||
schematic module, which you can instantiate like so:
|
||
```python
|
||
MyFile = Module("./MyFile.zen")
|
||
MyFile(
|
||
name = "MyFile",
|
||
...
|
||
)
|
||
```
|
||
|
||
### Load Resolution
|
||
|
||
The `load()` and `Module()` statements support multiple resolution strategies:
|
||
|
||
```python
|
||
# Local file (relative to current file)
|
||
load("./utils.zen", "helper")
|
||
|
||
# Package reference
|
||
load("@stdlib:1.2.3/math.zen", "calculate")
|
||
|
||
# GitHub repository
|
||
load("@github/user/repo:branch/path.zen", "function")
|
||
|
||
# GitLab repository
|
||
load("@gitlab/user/repo:branch/path.zen", "function")
|
||
|
||
# GitLab repository (nested groups)
|
||
Symbol(library = "@gitlab/kicad/libraries/kicad-symbols:v7.0.0/Device.kicad_sym", name = "R_US")
|
||
```
|
||
|
||
#### Default Package Aliases
|
||
|
||
Zener provides built-in package aliases for commonly used libraries:
|
||
|
||
- `@kicad-footprints` → `@gitlab/kicad/libraries/kicad-footprints:9.0.0`
|
||
- `@kicad-symbols` → `@gitlab/kicad/libraries/kicad-symbols:9.0.0`
|
||
- `@stdlib` → `@github/diodeinc/stdlib:HEAD`
|
||
|
||
These can be used directly:
|
||
|
||
```python
|
||
# Load from stdlib
|
||
load("@stdlib/units.zen", "kohm", "uF")
|
||
|
||
# Load from KiCad symbols library
|
||
R_symbol = Symbol(library = "@kicad-symbols/Device.kicad_sym", name = "R")
|
||
```
|
||
|
||
#### Custom Package Aliases
|
||
|
||
You can define custom package aliases or override the defaults in your workspace's `pcb.toml`:
|
||
|
||
```toml
|
||
[packages]
|
||
# Override default version
|
||
kicad-symbols = "@gitlab/kicad/libraries/kicad-symbols:7.0.0"
|
||
|
||
# Add custom aliases
|
||
my-lib = "@github/myorg/mylib:v1.0.0"
|
||
local-lib = "./path/to/local/lib"
|
||
```
|
||
|
||
## Core Types
|
||
|
||
### Net
|
||
|
||
A `Net` represents an electrical connection between component pins. Nets can optionally specify electrical properties like impedance, voltage range, and schematic symbols.
|
||
|
||
```python
|
||
# Create a net with optional name
|
||
net1 = Net()
|
||
net2 = Net("VCC")
|
||
|
||
# Net with impedance (for controlled impedance routing)
|
||
load("@stdlib/units.zen", "Impedance")
|
||
clk = Net("CLK", impedance=Impedance(50)) # 50Ω single-ended
|
||
|
||
# Net with voltage range
|
||
load("@stdlib/units.zen", "VoltageRange")
|
||
vdd = Net("VDD_3V3", voltage=VoltageRange("3.0V to 3.6V"))
|
||
|
||
# Net with custom schematic symbol
|
||
vcc_sym = Symbol(library="@kicad-symbols/power.kicad_sym", name="VCC")
|
||
vcc = Net("VCC", symbol=vcc_sym)
|
||
```
|
||
|
||
**Type**: `Net`
|
||
**Constructor**: `Net(name="", symbol=None, voltage=None, impedance=None)`
|
||
|
||
- `name` (optional): String identifier for the net
|
||
- `symbol` (optional): Symbol object for schematic representation
|
||
- `voltage` (optional): VoltageRange specification for the net
|
||
- `impedance` (optional): Impedance specification for single-ended nets (in Ohms)
|
||
|
||
### Symbol
|
||
|
||
A `Symbol` represents a schematic symbol definition with its pins. Symbols can be created manually or loaded from KiCad symbol libraries.
|
||
|
||
```python
|
||
# Create a symbol from explicit definition
|
||
my_symbol = Symbol(
|
||
name = "MyDevice",
|
||
definition = [
|
||
("VCC", ["1", "8"]), # VCC on pins 1 and 8
|
||
("GND", ["4"]), # GND on pin 4
|
||
("IN", ["2"]), # IN on pin 2
|
||
("OUT", ["7"]) # OUT on pin 7
|
||
]
|
||
)
|
||
|
||
# Load from a KiCad symbol library
|
||
op_amp = Symbol(library = "./symbols/LM358.kicad_sym")
|
||
|
||
# For multi-symbol libraries, specify which symbol
|
||
mcu = Symbol(library = "./symbols/microcontrollers.kicad_sym", name = "STM32F103")
|
||
|
||
# Shorthand syntax: library path and symbol name in one string
|
||
gnd = Symbol("@kicad-symbols/power.kicad_sym:GND")
|
||
resistor = Symbol("./symbols/passives.kicad_sym:R_0402")
|
||
|
||
# For single-symbol libraries, the name can be omitted
|
||
op_amp = Symbol("./symbols/LM358.kicad_sym")
|
||
```
|
||
|
||
**Type**: `Symbol`
|
||
**Constructor**: `Symbol(library_spec=None, name=None, definition=None, library=None)`
|
||
|
||
- `library_spec`: (positional) String in format "library_path:symbol_name" or just "library_path" for single-symbol libraries
|
||
- `name`: Symbol name (required when loading from multi-symbol library with named parameters)
|
||
- `definition`: List of (signal_name, [pad_numbers]) tuples
|
||
- `library`: Path to KiCad symbol library file
|
||
|
||
Note: You cannot mix the positional `library_spec` argument with the named `library` or `name` parameters.
|
||
|
||
### Component
|
||
|
||
Components represent physical electronic parts with pins and properties.
|
||
|
||
```python
|
||
# Using a Symbol for pin definitions
|
||
my_symbol = Symbol(
|
||
definition = [
|
||
("VCC", ["1"]),
|
||
("GND", ["4"]),
|
||
("OUT", ["8"])
|
||
]
|
||
)
|
||
|
||
Component(
|
||
name = "U1", # Required: instance name
|
||
footprint = "SOIC-8", # Required: PCB footprint
|
||
symbol = my_symbol, # Symbol defines the pins
|
||
pins = { # Required: pin connections
|
||
"VCC": vcc_net,
|
||
"GND": gnd_net,
|
||
"OUT": output_net
|
||
},
|
||
prefix = "U", # Optional: reference designator prefix (default: "U")
|
||
mpn = "LM358", # Optional: manufacturer part number
|
||
type = "op-amp", # Optional: component type
|
||
properties = { # Optional: additional properties
|
||
"voltage": "5V"
|
||
}
|
||
)
|
||
```
|
||
|
||
**Type**: `Component`
|
||
**Constructor**: `Component(**kwargs)`
|
||
|
||
Key parameters:
|
||
|
||
- `name`: Instance name (required)
|
||
- `footprint`: PCB footprint (required)
|
||
- `symbol`: Symbol object defining pins (required)
|
||
- `pins`: Pin connections to nets (required)
|
||
- `prefix`: Reference designator prefix (default: "U")
|
||
- `mpn`: Manufacturer part number
|
||
- `type`: Component type
|
||
- `properties`: Additional properties dict
|
||
|
||
### Interface
|
||
|
||
Interfaces define reusable connection patterns with field specifications and type validation. Interfaces can specify impedance requirements that automatically propagate to their constituent nets during layout.
|
||
|
||
**Differential Pair Example:**
|
||
|
||
```python
|
||
load("@stdlib/units.zen", "Impedance")
|
||
|
||
# DiffPair interface with 90Ω differential impedance
|
||
DiffPair = interface(
|
||
P=Net(),
|
||
N=Net(),
|
||
impedance=field(Impedance, None), # Optional impedance
|
||
)
|
||
|
||
# USB interface uses DiffPair with 90Ω impedance
|
||
Usb2 = interface(
|
||
D=DiffPair(impedance=Impedance(90)),
|
||
)
|
||
|
||
# When instantiated, impedance automatically propagates to P/N nets
|
||
usb = Usb2("USB") # USB.D.P and USB.D.N both get 90Ω differential impedance
|
||
```
|
||
|
||
The impedance from DiffPair interfaces is stored as `differential_impedance` on the P and N nets, allowing the layout system to assign appropriate netclasses for differential pair routing.
|
||
|
||
#### Basic Syntax
|
||
|
||
```python
|
||
InterfaceName = interface(
|
||
field_name = field_specification,
|
||
__post_init__ = callback_function, # Optional
|
||
)
|
||
```
|
||
|
||
#### Field Types
|
||
|
||
**Net Instances**: Use the provided Net instance as the default template
|
||
```python
|
||
NET = Net("VCC", symbol = Symbol(library = "@kicad-symbols/power.kicad_sym", name = "VCC"))
|
||
SDA = Net("I2C_SDA")
|
||
```
|
||
|
||
**Interface Instances**: Use the provided interface instance for composition
|
||
```python
|
||
uart = Uart(TX=Net("UART_TX"), RX=Net("UART_RX"))
|
||
power = Power(NET=Net("VDD"))
|
||
```
|
||
|
||
**field() Specifications**: Enforce type checking with explicit defaults
|
||
```python
|
||
voltage = field(Voltage, unit("3.3V", Voltage))
|
||
freqs = field(list[str], ["100kHz", "400kHz"])
|
||
count = field(int, 42)
|
||
```
|
||
|
||
#### Interface Instantiation
|
||
|
||
```python
|
||
InterfaceName([name], field1=value1, field2=value2, ...)
|
||
```
|
||
|
||
- **Optional name**: First positional argument sets the interface instance name
|
||
- **Field overrides**: Named parameters override defaults
|
||
- **Type validation**: Values must match field specifications
|
||
|
||
#### Examples
|
||
|
||
```python
|
||
# Define interfaces
|
||
Power = interface(
|
||
NET = Net("VCC", symbol = Symbol(library = "@kicad-symbols/power.kicad_sym", name = "VCC")),
|
||
voltage = field(Voltage, unit("3.3V", Voltage)),
|
||
)
|
||
|
||
Uart = interface(
|
||
TX = Net("UART_TX"),
|
||
RX = Net("UART_RX"),
|
||
)
|
||
|
||
SystemInterface = interface(
|
||
power = Power(),
|
||
uart = Uart(),
|
||
debug = field(bool, False),
|
||
)
|
||
|
||
# Create instances
|
||
power = Power() # All defaults
|
||
vcc = Power("VCC_3V3") # Named instance
|
||
custom = Power("MAIN", voltage=unit("5V", Voltage)) # Named with override
|
||
|
||
system = SystemInterface("MAIN", debug=True)
|
||
```
|
||
|
||
#### Post-Initialization Callbacks
|
||
|
||
```python
|
||
def _power_post_init(self):
|
||
if self.voltage.value <= 0:
|
||
error("Power voltage must be positive")
|
||
|
||
Power = interface(
|
||
NET = Net("VCC"),
|
||
voltage = field(Voltage, unit("3.3V", Voltage)),
|
||
__post_init__ = _power_post_init,
|
||
)
|
||
```
|
||
|
||
Callbacks receive `self` and cannot be overridden during instantiation.
|
||
|
||
**Type**: `interface`
|
||
**Constructor**: `interface(**fields)`
|
||
|
||
- Fields can be Net instances, interface instances, or `field()` specifications
|
||
|
||
### Module
|
||
|
||
Modules represent hierarchical subcircuits that can be instantiated multiple times. Module objects support indexing to access child components and submodules directly.
|
||
|
||
```python
|
||
# Load a module from a file
|
||
SubCircuit = Module("./subcircuit.zen")
|
||
|
||
# Instantiate the module
|
||
SubCircuit(
|
||
name = "power_supply",
|
||
# ... pass inputs defined by io() and config() in the module
|
||
)
|
||
|
||
# Access child components and submodules via indexing
|
||
component = module["ComponentName"] # Access component by name
|
||
submodule = module["SubmoduleName"] # Access submodule by name
|
||
|
||
# Chain indexing for nested access
|
||
nested_component = module["SubmoduleName"]["ComponentName"]
|
||
|
||
# Nested path syntax (equivalent to chained indexing)
|
||
nested_component = module["SubmoduleName.ComponentName"] # Same as above
|
||
|
||
# Check if components exist using membership operator
|
||
if "ComponentName" in module:
|
||
component = module["ComponentName"]
|
||
|
||
# Check nested paths
|
||
if "SubmoduleName.ComponentName" in module:
|
||
nested_component = module["SubmoduleName.ComponentName"]
|
||
```
|
||
|
||
**Type**: `Module`
|
||
**Constructor**: `Module(path)`
|
||
|
||
**Module Indexing**: `module[name]` supports:
|
||
- Single names: `module["ComponentName"]` returns Component or Module objects
|
||
- Nested paths: `module["Sub.Component"]` equivalent to `module["Sub"]["Component"]`
|
||
- Deep nesting: `module["A.B.C"]` equivalent to `module["A"]["B"]["C"]`
|
||
- Returns Component objects for leaf components, Module objects for intermediate submodules
|
||
- Raises an error if any part of the path is not found
|
||
|
||
**Module Membership**: `name in module` supports:
|
||
- Single names: `"ComponentName" in module` checks if component or submodule exists
|
||
- Nested paths: `"Sub.Component" in module` equivalent to checking nested existence
|
||
- Returns `True` if the path exists, `False` otherwise
|
||
- Works with the same path syntax as indexing
|
||
|
||
**Module Attributes**:
|
||
- `module.nets`: Dict mapping net names to lists of connected port tuples
|
||
- `module.components`: Dict mapping component paths to component objects
|
||
|
||
**Naming Conventions**:
|
||
- Component paths in `module.components` follow the pattern `SubmoduleName.ComponentName` (e.g., `"BMI270.BMI270"`, `"C1.C"`)
|
||
- The first part is the submodule name, the second part is the component name within that submodule
|
||
- Indexing supports both single names and nested paths:
|
||
- `module["BMI270"]` returns the BMI270 submodule
|
||
- `module["BMI270"]["BMI270"]` returns the component within the submodule (chained)
|
||
- `module["BMI270.BMI270"]` also returns the component (nested path syntax)
|
||
- All three approaches are equivalent for accessing nested components
|
||
|
||
### TestBench
|
||
|
||
TestBench values represent the results of module validation tests. They are created by the `TestBench()` function and contain information about the tested module and check results.
|
||
|
||
```python
|
||
# TestBench values are returned by TestBench()
|
||
result = TestBench(
|
||
name = "MyTest",
|
||
module = MyModule,
|
||
checks = [check_func1, check_func2]
|
||
)
|
||
|
||
# Access TestBench properties
|
||
print(result) # TestBench(MyTest)
|
||
```
|
||
|
||
**Type**: `TestBench`
|
||
**Created by**: `TestBench()` function (see [Built-in Functions](#testbenchname-module-checks))
|
||
|
||
Properties accessible via the TestBench value:
|
||
- `name`: The test bench identifier
|
||
- Module evaluation status
|
||
- Check function results
|
||
|
||
### PhysicalRange
|
||
|
||
A `PhysicalRange` represents a bounded range of physical values with an optional nominal value, used for specifying operating ranges, tolerances, and electrical characteristics. Ranges have minimum and maximum bounds with consistent physical units.
|
||
|
||
```python
|
||
# PhysicalRange values are created by unit-specific range constructors
|
||
# (See builtin.physical_range() in Built-in Functions)
|
||
|
||
# String parsing with range separators
|
||
voltage_range = VoltageRange("1.1–3.6V") # 1.1–3.6 V
|
||
supply_range = VoltageRange("11V to 26V") # 11–26 V
|
||
|
||
# With nominal value in parentheses
|
||
input_range = VoltageRange("11–26 V (12 V nom.)") # 11–26 V (12 V nom.)
|
||
|
||
# Tolerance expansion
|
||
nominal_voltage = VoltageRange("15V 10%") # 13.5–16.5 V
|
||
|
||
# From PhysicalValue
|
||
single_point = VoltageRange(Voltage(15)) # 15–15 V
|
||
|
||
# Keyword arguments
|
||
operating_range = VoltageRange(min=11, nominal=16, max=26) # 11–26 V (16 V nom.)
|
||
|
||
# Mixed string/number keywords
|
||
custom_range = VoltageRange(min="11V", nominal="16", max="26V") # 11–26 V (16 V nom.)
|
||
|
||
# Override nominal separately
|
||
with_nominal = VoltageRange("11V to 26V", nominal="16V") # 11–26 V (16 V nom.)
|
||
```
|
||
|
||
**Type**: `PhysicalRange`
|
||
**Created by**: Unit-specific range constructors (e.g., `VoltageRange`, `CurrentRange`)
|
||
|
||
Properties:
|
||
- `min`: Minimum value (Decimal)
|
||
- `max`: Maximum value (Decimal)
|
||
- `nominal`: Optional nominal/typical value (Decimal or None)
|
||
- `unit`: Physical unit dimensions
|
||
|
||
**Display Format**:
|
||
- With nominal: `11–26 V (12 V nom.)`
|
||
- Without nominal: `11–26 V`
|
||
|
||
#### PhysicalRangeType
|
||
|
||
A `PhysicalRangeType` is a factory type that creates `PhysicalRange` instances for a specific physical unit. Each unit has its own range type (e.g., `VoltageRange` for volts, `CurrentRange` for amperes).
|
||
|
||
```python
|
||
# Range types are created by builtin.physical_range()
|
||
VoltageRange = builtin.physical_range("V")
|
||
CurrentRange = builtin.physical_range("A")
|
||
|
||
# Range types are callable and return PhysicalRange instances
|
||
input_voltage = VoltageRange("3.3V to 5V")
|
||
load_current = CurrentRange("100mA to 500mA")
|
||
```
|
||
|
||
**Type**: `PhysicalRangeType`
|
||
**Created by**: `builtin.physical_range(unit)` (see [Built-in Functions](#builtinphysical_rangeunit))
|
||
|
||
**Standard Range Types** (available in `@stdlib/units.zen`):
|
||
- `VoltageRange` - Voltage ranges (V)
|
||
- `CurrentRange` - Current ranges (A)
|
||
- `ResistanceRange` - Resistance ranges (Ω)
|
||
- `CapacitanceRange` - Capacitance ranges (F)
|
||
- `InductanceRange` - Inductance ranges (H)
|
||
- `FrequencyRange` - Frequency ranges (Hz)
|
||
- `TemperatureRange` - Temperature ranges (K)
|
||
- `TimeRange` - Time ranges (s)
|
||
- `PowerRange` - Power ranges (W)
|
||
|
||
**Construction Flexibility**:
|
||
|
||
Range constructors accept multiple input formats for maximum flexibility:
|
||
|
||
1. **String parsing with separators**:
|
||
- En-dash: `"1.1–3.6V"`
|
||
- Word "to": `"11V to 26V"`
|
||
- Left side can omit unit: `"11–26V"`
|
||
|
||
2. **Nominal value extraction**:
|
||
- Parenthesized suffix: `"11–26 V (12 V nom.)"`
|
||
- Can be combined with string ranges or keyword override
|
||
|
||
3. **Tolerance expansion**:
|
||
- Percentage tolerance: `"15V 10%"` expands to `13.5–16.5 V`
|
||
|
||
4. **PhysicalValue conversion**:
|
||
- Single value: `VoltageRange(Voltage(15))` creates point range `15–15 V`
|
||
- Toleranced value: `VoltageRange(Voltage(15, 0.1))` expands tolerance to range
|
||
|
||
5. **Keyword arguments**:
|
||
- Explicit bounds: `min=11, max=26`
|
||
- With nominal: `min=11, nominal=16, max=26`
|
||
- Accepts numbers or strings: `min="11V"`, `max="26V"`
|
||
|
||
6. **Mixed modes**:
|
||
- String range with keyword nominal: `VoltageRange("11V to 26V", nominal="16V")`
|
||
|
||
**Type Safety**:
|
||
- Units must be consistent across min, max, and nominal values
|
||
- Range types are distinct: `VoltageRange` ≠ `CurrentRange`
|
||
- Type checking enforces unit correctness at compile time
|
||
|
||
**Use Cases**:
|
||
- Component operating ranges: `input_voltage = VoltageRange("2.7V to 5.5V")`
|
||
- Load specifications: `output_current = CurrentRange("0A to 3A")`
|
||
- Environmental conditions: `operating_temp = TemperatureRange("-40C to 85C")`
|
||
- Future: Electrical Rule Checking (ERC) - validate that supply ranges satisfy load requirements
|
||
|
||
## Built-in Functions
|
||
|
||
### moved(old_path, new_path)
|
||
|
||
**Parameters**
|
||
|
||
- `old_path`: The old path that should be remapped
|
||
- `new_path`: The new path to remap to
|
||
|
||
The `moved()` directive allows you to specify path remapping for refactoring support. When modules or components are moved or renamed, downstream artifacts (like layout files) may still reference the old paths. The `moved()` directive provides a mapping from old paths to new paths.
|
||
|
||
`moved()` directives are scoped to the module where they are defined. They affect how paths from that module are resolved in downstream artifacts.
|
||
|
||
If the path refers to a module, all children of that module are automatically remapped:
|
||
|
||
```python
|
||
# This single directive handles:
|
||
# POW.PS1 -> PS1
|
||
# POW.PS1.some_resistor -> PS1.some_resistor
|
||
# POW.PS1.inner.component -> PS1.inner.component
|
||
moved("POW.PS1", "PS1")
|
||
```
|
||
|
||
```python
|
||
# Moved a power supply module from POW namespace to root
|
||
moved("POW.PS1", "PS1")
|
||
|
||
# Renamed a component
|
||
moved("OLD_COMP", "NEW_COMP")
|
||
|
||
# Moved components between modules
|
||
moved("ModuleA.ComponentX", "ModuleB.ComponentX")
|
||
|
||
# Net remapping for position comments
|
||
moved("AN_OLD_FILTERED_VCC_VCC", "FILTERED_VCC_VCC")
|
||
```
|
||
|
||
### io(name, type, checks=None, default=None, optional=False)
|
||
|
||
Declares a net or interface input for a module.
|
||
|
||
```python
|
||
# Required net input
|
||
vcc = io("vcc", Net)
|
||
|
||
# Optional interface input with default
|
||
PowerInterface = interface(vcc = Net, gnd = Net)
|
||
power = io("power", PowerInterface, optional=True)
|
||
|
||
# With explicit default
|
||
data = io("data", Net, default=Net("DATA"))
|
||
|
||
# With validation checks (single function)
|
||
def check_voltage_within(within):
|
||
def check_fn(power_value):
|
||
voltage = power_value.voltage
|
||
check(voltage.min >= within.min and voltage.max <= within.max,
|
||
f"Voltage {voltage} not within {within}")
|
||
return check_fn
|
||
|
||
vbat = io("VBAT", Power, check_voltage_within(VoltageRange("10–30 V")))
|
||
|
||
# Or with multiple checks as a list
|
||
vbat = io("VBAT", Power, [check_voltage_within(VoltageRange("10–30 V")), check_current()])
|
||
|
||
# Or as a named argument
|
||
vbat = io("VBAT", Power, checks=check_voltage_within(VoltageRange("10–30 V")))
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `name`: String identifier for the input (positional)
|
||
- `type`: Expected type - `Net` or interface type (positional)
|
||
- `checks`: Optional check function or list of check functions to run on the value at eval time (3rd positional or named)
|
||
- `default`: Default value if not provided by parent (named)
|
||
- `optional`: If True, returns None when not provided (unless default is specified) (named)
|
||
|
||
### config(name, type, default=None, convert=None, optional=False)
|
||
|
||
Declares a configuration value input for a module.
|
||
|
||
```python
|
||
# String configuration
|
||
prefix = config("prefix", str, default="U")
|
||
|
||
# Integer with conversion
|
||
baudrate = config("baudrate", int, convert=int)
|
||
|
||
# Enum configuration
|
||
Direction = enum("NORTH", "SOUTH", "EAST", "WEST")
|
||
heading = config("heading", Direction)
|
||
|
||
# Optional configuration
|
||
debug = config("debug", bool, optional=True)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `name`: String identifier for the input
|
||
- `type`: Expected type (str, int, float, bool, enum, or record type)
|
||
- `default`: Default value if not provided
|
||
- `convert`: Optional conversion function
|
||
- `optional`: If True, returns None when not provided (unless default is specified)
|
||
|
||
### File(path)
|
||
|
||
Resolves a file or directory path using the load resolver.
|
||
|
||
```python
|
||
# Get absolute path to a file
|
||
config_path = File("./config.json")
|
||
|
||
# Works with load resolver syntax
|
||
stdlib_path = File("@stdlib/components")
|
||
```
|
||
|
||
### Path(path, allow_not_exist=False)
|
||
|
||
Advanced path resolution supporting all LoadSpec formats with optional existence checking.
|
||
|
||
```python
|
||
# Basic path resolution (same as File)
|
||
config_path = Path("./config.json")
|
||
|
||
# LoadSpec format support
|
||
stdlib_path = Path("@stdlib/components")
|
||
github_path = Path("@github/user/repo:branch/path.zen")
|
||
|
||
# Allow non-existent paths (only works with local paths)
|
||
optional_file = Path("./optional.zen", allow_not_exist=True)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `path`: String path to resolve (supports any LoadSpec format)
|
||
- `allow_not_exist`: Optional boolean (default: False). If True, allows non-existent paths. Can only be used with local path LoadSpecs, not package/GitHub/GitLab specs.
|
||
|
||
### error(msg)
|
||
|
||
Raises a runtime error with the given message.
|
||
|
||
```python
|
||
if not condition:
|
||
error("Condition failed")
|
||
```
|
||
|
||
### check(condition, msg)
|
||
|
||
Checks a condition and raises an error if false.
|
||
|
||
```python
|
||
check(voltage > 0, "Voltage must be positive")
|
||
check(len(pins) == 8, "Expected 8 pins")
|
||
```
|
||
|
||
### add_property(name, value)
|
||
|
||
Adds a property to the current module instance.
|
||
|
||
```python
|
||
add_property("layout_group", "power_supply")
|
||
add_property("critical", True)
|
||
```
|
||
|
||
### builtin.net(type_name, **fields)
|
||
|
||
**Built-in function** that creates custom typed net constructors with optional field parameters.
|
||
|
||
```python
|
||
# Create a basic net type (equivalent to the builtin Net)
|
||
Net = builtin.net("Net")
|
||
|
||
# Create a typed net with field() specifications (with defaults)
|
||
Power = builtin.net("Power",
|
||
voltage=field(str, "3.3V"),
|
||
max_current=field(int, 1000)
|
||
)
|
||
|
||
# Create a typed net with direct type constructors (no defaults)
|
||
Signal = builtin.net("Signal", frequency=int, slew_rate=str)
|
||
|
||
# Create a typed net with physical value fields
|
||
load("@stdlib/units.zen", "Voltage", "VoltageRange")
|
||
Supply = builtin.net("Supply",
|
||
voltage=Voltage,
|
||
range=VoltageRange
|
||
)
|
||
|
||
# Create a typed net with enum fields
|
||
Level = enum("LOW", "HIGH")
|
||
DigitalSignal = builtin.net("DigitalSignal", level=Level)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `type_name`: String name for the net type (required, positional)
|
||
- `**fields`: Keyword arguments defining optional typed fields
|
||
- Each field can be a `field()` spec (with default), a type constructor (str/int/float/bool), an enum type, or a physical value type
|
||
|
||
**Field Specifications:**
|
||
|
||
Fields accept any of these specifications:
|
||
|
||
- `field(type, default)` - Typed field with a default value (from starlark's builtin `field()`)
|
||
- `str`, `int`, `float`, `bool` - Direct type constructors (no default)
|
||
- `EnumType` - Enum type created with `enum()`
|
||
- `Voltage`, `Current`, etc. - Physical value types from stdlib
|
||
|
||
**Usage:**
|
||
|
||
All fields are **optional** during net instantiation - you can create a net without providing any field values:
|
||
|
||
```python
|
||
Power = builtin.net("Power", voltage=field(str, "3.3V"), max_current=field(int, 1000))
|
||
|
||
# Provide all fields
|
||
vcc = Power("VCC", voltage="5V", max_current=2000)
|
||
|
||
# Provide some fields (others get defaults from field() specs)
|
||
vdd = Power("VDD", voltage="3.3V") # max_current uses default 1000
|
||
|
||
# Provide no fields (all use defaults from field() specs)
|
||
v3v3 = Power("3V3") # voltage="3.3V", max_current=1000
|
||
|
||
# Access field values as attributes
|
||
print(vcc.voltage) # "5V"
|
||
print(vcc.max_current) # 2000
|
||
```
|
||
|
||
**Field Access:**
|
||
|
||
- Fields can be accessed as attributes on net instances: `net.field_name`
|
||
- If a field was not provided and has no default, accessing it will fail
|
||
- Fields are stored in the net's properties and can be introspected
|
||
|
||
**Type Validation:**
|
||
|
||
- Field values are validated at net instantiation time using starlark's type system
|
||
- Type mismatches produce clear error messages:
|
||
```python
|
||
Power = builtin.net("Power", voltage=str)
|
||
vcc = Power("VCC", voltage=123) # Error: Field 'voltage' has wrong type: expected str, got int
|
||
```
|
||
- Validation works uniformly for all type specifications: builtin types, field() wrappers, enums, and custom types
|
||
|
||
**Typical Use Cases:**
|
||
|
||
```python
|
||
# Power nets with voltage specifications
|
||
Power = builtin.net("Power", voltage=field(str, "3.3V"))
|
||
vcc_3v3 = Power("VCC_3V3", voltage="3.3V")
|
||
vcc_5v = Power("VCC_5V", voltage="5V")
|
||
|
||
# Clock signals with frequency metadata
|
||
Clock = builtin.net("Clock", frequency=int)
|
||
main_clk = Clock("MAIN_CLK", frequency=8000000)
|
||
rtc_clk = Clock("RTC_CLK", frequency=32768)
|
||
|
||
# Digital signals with logic level
|
||
Level = enum("1V8", "3V3", "5V")
|
||
Gpio = builtin.net("Gpio", level=Level)
|
||
gpio_pin = Gpio("GPIO_1", level=Level("3V3"))
|
||
|
||
# Power rails with electrical specifications
|
||
load("@stdlib/units.zen", "Voltage", "Current", "unit")
|
||
PowerRail = builtin.net("PowerRail",
|
||
nominal_voltage=Voltage,
|
||
max_current=Current
|
||
)
|
||
main_rail = PowerRail("MAIN_RAIL",
|
||
nominal_voltage=unit("3.3V", Voltage),
|
||
max_current=unit("2A", Current)
|
||
)
|
||
```
|
||
|
||
**Returns:** A callable net type constructor that creates typed net instances
|
||
|
||
### builtin.physical_range(unit)
|
||
|
||
**Built-in function** that creates unit-specific range constructor types for defining bounded physical value ranges.
|
||
|
||
```python
|
||
# Create range constructors for different units
|
||
VoltageRange = builtin.physical_range("V")
|
||
CurrentRange = builtin.physical_range("A")
|
||
ResistanceRange = builtin.physical_range("Ohm")
|
||
CapacitanceRange = builtin.physical_range("F")
|
||
InductanceRange = builtin.physical_range("H")
|
||
FrequencyRange = builtin.physical_range("Hz")
|
||
TemperatureRange = builtin.physical_range("K")
|
||
TimeRange = builtin.physical_range("s")
|
||
PowerRange = builtin.physical_range("W")
|
||
|
||
# Use the constructors to create ranges
|
||
input_voltage = VoltageRange("2.7V to 5.5V")
|
||
output_current = CurrentRange("0A to 3A")
|
||
operating_temp = TemperatureRange("-40C to 85C")
|
||
|
||
# All construction patterns are supported
|
||
supply = VoltageRange("11–26 V (12 V nom.)") # String with nominal
|
||
tolerance = VoltageRange("15V 10%") # Tolerance expansion
|
||
explicit = VoltageRange(min=11, nominal=16, max=26) # Keyword arguments
|
||
from_value = VoltageRange(Voltage(15)) # From PhysicalValue
|
||
mixed = VoltageRange("11V to 26V", nominal="16V") # Mixed modes
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `unit`: String identifier for the physical unit (e.g., `"V"`, `"A"`, `"Ohm"`, `"F"`, `"H"`, `"Hz"`, `"K"`, `"s"`, `"W"`)
|
||
|
||
**Returns:** A `PhysicalRangeType` that can be called to create `PhysicalRange` instances
|
||
|
||
**Standard Usage:**
|
||
|
||
This builtin is typically accessed through `@stdlib/units.zen`, which provides pre-defined range constructors:
|
||
|
||
```python
|
||
load("@stdlib/units.zen", "VoltageRange", "CurrentRange", "ResistanceRange")
|
||
|
||
# Use standard constructors from stdlib
|
||
input_range = VoltageRange("3.3V to 5V")
|
||
load_range = CurrentRange("100mA to 500mA")
|
||
pullup_range = ResistanceRange("4.7kOhm to 10kOhm")
|
||
```
|
||
|
||
**Available in stdlib/units.zen:**
|
||
- `VoltageRange` - Voltage ranges (V)
|
||
- `CurrentRange` - Current ranges (A)
|
||
- `ResistanceRange` - Resistance ranges (Ω)
|
||
- `CapacitanceRange` - Capacitance ranges (F)
|
||
- `InductanceRange` - Inductance ranges (H)
|
||
- `FrequencyRange` - Frequency ranges (Hz)
|
||
- `TemperatureRange` - Temperature ranges (K)
|
||
- `TimeRange` - Time ranges (s)
|
||
- `PowerRange` - Power ranges (W)
|
||
|
||
**Construction Patterns:**
|
||
|
||
The returned range constructor accepts flexible input formats:
|
||
|
||
```python
|
||
# String parsing with range separators
|
||
VoltageRange("1.1–3.6V") # En-dash separator
|
||
VoltageRange("11V to 26V") # Word "to" separator
|
||
VoltageRange("11–26V") # Left side can omit unit
|
||
|
||
# Nominal value in parentheses
|
||
VoltageRange("11–26 V (12 V nom.)")
|
||
|
||
# Tolerance expansion
|
||
VoltageRange("15V 10%") # Expands to 13.5–16.5 V
|
||
|
||
# From PhysicalValue
|
||
VoltageRange(Voltage(15)) # Single point: 15–15 V
|
||
VoltageRange(Voltage(15, 0.1)) # With tolerance: 13.5–16.5 V
|
||
|
||
# Keyword arguments (all must be present)
|
||
VoltageRange(min=11, max=26)
|
||
VoltageRange(min=11, nominal=16, max=26)
|
||
|
||
# Mixed string/number keywords
|
||
VoltageRange(min="11V", nominal="16", max="26V")
|
||
|
||
# Override nominal separately
|
||
VoltageRange("11V to 26V", nominal="16V")
|
||
```
|
||
|
||
**Type Safety:**
|
||
|
||
- Units must be consistent: `VoltageRange("5V to 3A")` → Error (mixing V and A)
|
||
- Range types are distinct: Cannot pass a `VoltageRange` where `CurrentRange` is expected
|
||
- Type checking enforces correctness at compile time
|
||
|
||
**Use Cases:**
|
||
|
||
Physical ranges are used to specify operating characteristics, constraints, and requirements:
|
||
|
||
```python
|
||
# Component specifications
|
||
input_voltage = VoltageRange("2.7V to 5.5V")
|
||
output_current = CurrentRange("0A to 3A (1.5A nom.)")
|
||
operating_temp = TemperatureRange("-40C to 85C")
|
||
|
||
# Load requirements
|
||
supply_current = CurrentRange("10mA to 100mA")
|
||
clock_frequency = FrequencyRange("1MHz to 100MHz")
|
||
|
||
# Tolerance specifications
|
||
capacitance = CapacitanceRange("100nF 10%") # 90nF to 110nF
|
||
resistance = ResistanceRange("10kOhm 5%") # 9.5kΩ to 10.5kΩ
|
||
|
||
# Future: Electrical Rule Checking (ERC)
|
||
# Validate that supply ranges satisfy load requirements
|
||
# check(supply_voltage_range.contains(load_voltage_range))
|
||
```
|
||
|
||
See [PhysicalRange](#physicalrange) in Core Types for detailed information about the returned range values.
|
||
|
||
### builtin.add_board_config(name, config, default=False)
|
||
|
||
**Built-in function** for registering board configurations with the layout system.
|
||
|
||
**Parameters:**
|
||
- `name`: String identifier for the board configuration
|
||
- `config`: `BoardConfig` object containing design rules and stackup
|
||
- `default`: If True, this becomes the default board config for the project
|
||
|
||
This builtin is typically called through the stdlib `Board()` function rather than directly.
|
||
|
||
### builtin.add_electrical_check(name, check_fn, inputs=None)
|
||
|
||
**Built-in function** for registering electrical validation checks that run during `pcb build`.
|
||
|
||
**Parameters:**
|
||
- `name`: String identifier for the check (required)
|
||
- `check_fn`: Function to execute for validation (required)
|
||
- `inputs`: Optional dictionary of input parameters to pass to the check function
|
||
|
||
Electrical checks use lazy evaluation - they are registered during module evaluation but execute after the design is fully evaluated. This allows checks to validate electrical properties, design rules, or other constraints across the entire design.
|
||
|
||
The check function receives the module as its first argument, followed by any specified inputs as keyword arguments:
|
||
|
||
```python
|
||
def voltage_range_check(module, min_voltage, max_voltage):
|
||
"""Check that supply voltage is within acceptable range"""
|
||
supply = module.supply_voltage
|
||
if supply < min_voltage or supply > max_voltage:
|
||
error("Supply voltage {} is outside range {}-{}".format(
|
||
supply, min_voltage, max_voltage
|
||
))
|
||
|
||
builtin.add_electrical_check(
|
||
name="supply_voltage_range",
|
||
check_fn=voltage_range_check,
|
||
inputs={
|
||
"min_voltage": 3.0,
|
||
"max_voltage": 5.5,
|
||
}
|
||
)
|
||
```
|
||
|
||
**Check Function Signature:**
|
||
```python
|
||
def check_function(module, **kwargs):
|
||
# Validation logic
|
||
# Raise error() or fail assertion to indicate failure
|
||
pass
|
||
```
|
||
|
||
**Example - Basic Check:**
|
||
```python
|
||
def check_no_floating_nets(module):
|
||
"""Ensure all nets are connected to at least 2 pins"""
|
||
for net in module.nets:
|
||
if len(net.pins) < 2:
|
||
error("Net '{}' is floating (only {} pin connected)".format(
|
||
net.name, len(net.pins)
|
||
))
|
||
|
||
builtin.add_electrical_check(
|
||
name="no_floating_nets",
|
||
check_fn=check_no_floating_nets
|
||
)
|
||
```
|
||
|
||
**Example - Parameterized Check:**
|
||
```python
|
||
def check_power_capacity(module, max_current):
|
||
"""Verify power supply can handle load current"""
|
||
total_load = sum([c.max_current for c in module.components])
|
||
if total_load > max_current:
|
||
error("Total load current {}A exceeds supply capacity {}A".format(
|
||
total_load, max_current
|
||
))
|
||
|
||
builtin.add_electrical_check(
|
||
name="power_capacity",
|
||
check_fn=check_power_capacity,
|
||
inputs={"max_current": 3.0}
|
||
)
|
||
```
|
||
|
||
**Execution Model:**
|
||
|
||
1. Checks are registered during module evaluation via `builtin.add_electrical_check()`
|
||
2. Check functions and inputs are stored as frozen values
|
||
3. During `pcb build`, after evaluation completes, all checks are collected from the module tree
|
||
4. Each check executes with a fresh evaluator context
|
||
5. Check failures generate error diagnostics that are reported through the standard diagnostic system
|
||
6. Build fails if any electrical checks fail
|
||
|
||
**Notes:**
|
||
- Checks run only during `pcb build`, not during `pcb test` (use `TestBench` for test-specific validation)
|
||
- Check failures generate error-level diagnostics
|
||
- Checks can access the entire module structure, including components, nets, interfaces, and properties
|
||
- Multiple checks with the same name are allowed (they execute independently)
|
||
|
||
## Board Configuration Types
|
||
|
||
The board configuration record types are provided by `@stdlib/board_config.zen` and offer comprehensive control over PCB manufacturing specifications.
|
||
|
||
**Available Records and Functions:**
|
||
|
||
```python
|
||
load("@stdlib/board_config.zen",
|
||
"Board", # Board configuration function
|
||
"BoardConfig", # Main configuration container
|
||
"DesignRules", # Design rule constraints
|
||
"Stackup", # Layer stackup definition
|
||
"Material", # Material properties
|
||
"CopperLayer", # Copper layer specification
|
||
"DielectricLayer", # Dielectric layer specification
|
||
"Constraints", # Manufacturing constraints
|
||
"Copper", # Copper-specific rules
|
||
"NetClass", # Netclass definitions
|
||
"merge_configs", # Configuration merging utility
|
||
"deep_merge" # Deep merge utility
|
||
)
|
||
```
|
||
|
||
### Board(name, layout_path, config=None, layers=None, layout_hints=None, default=False)
|
||
|
||
**Stdlib function** for declaring board configurations for PCB layout generation.
|
||
|
||
```python
|
||
# Load from stdlib
|
||
load("@stdlib/board_config.zen", "Board", "BoardConfig", "DesignRules", "Stackup", "Material", "CopperLayer", "DielectricLayer")
|
||
|
||
# Simple usage with predefined layer count (recommended for most projects)
|
||
Board(
|
||
name="MyBoard",
|
||
layout_path="layout/",
|
||
layers=4,
|
||
default=True
|
||
)
|
||
|
||
# Advanced usage with custom configuration
|
||
config = BoardConfig(
|
||
stackup=Stackup(
|
||
materials=[
|
||
Material(
|
||
name="FR-4",
|
||
vendor="Isola",
|
||
relative_permittivity=4.21,
|
||
loss_tangent=0.025,
|
||
reference_frequency=1e9,
|
||
),
|
||
],
|
||
silk_screen_color="#44805BFF",
|
||
solder_mask_color="#191919E6",
|
||
thickness=1.605,
|
||
symmetric=True,
|
||
copper_finish="ENIG",
|
||
layers=[
|
||
CopperLayer(thickness=0.035, role="mixed"),
|
||
DielectricLayer(thickness=0.2, material="FR-4", form="prepreg"),
|
||
CopperLayer(thickness=0.035, role="power"),
|
||
DielectricLayer(thickness=1.065, material="FR-4", form="core"),
|
||
CopperLayer(thickness=0.035, role="power"),
|
||
DielectricLayer(thickness=0.2, material="FR-4", form="prepreg"),
|
||
CopperLayer(thickness=0.035, role="mixed"),
|
||
],
|
||
)
|
||
)
|
||
|
||
# Declare board using custom configuration
|
||
Board(
|
||
name="MyBoard",
|
||
layout_path="layout/",
|
||
config=config,
|
||
default=True
|
||
)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `name`: String identifier for the board configuration (required)
|
||
- `layout_path`: Directory path where layout files will be generated (required)
|
||
- `config`: `BoardConfig` record containing design rules and stackup (optional, mutually exclusive with `layers`)
|
||
- `layers`: Number of layers - 2, 4, 6, or 8 (optional, mutually exclusive with `config`). When specified, uses predefined stackups from the stdlib:
|
||
- `layers=2`: 2-layer board (1.6mm, 1oz copper, SIG/SIG)
|
||
- `layers=4`: 4-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/PWR/SIG)
|
||
- `layers=6`: 6-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/(SIG/PWR)/(SIG/PWR)/GND/SIG)
|
||
- `layers=8`: 8-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/SIG/PWR/GND/SIG/GND/SIG)
|
||
- `layout_hints`: Optional list of layout optimization hints
|
||
- `default`: If True, this becomes the default board config for the project
|
||
|
||
**Note:** Either `config` or `layers` must be provided, but not both. If neither is provided, an error will be raised.
|
||
|
||
**Implementation:** The stdlib `Board()` function calls the language builtin `builtin.add_board_config()` and sets layout properties.
|
||
|
||
**BoardConfig Structure:**
|
||
|
||
```python
|
||
BoardConfig = record(
|
||
design_rules=DesignRules, # PCB design rules and constraints
|
||
stackup=Stackup, # Layer stackup and materials
|
||
num_user_layers=int, # Number of User.N layers (default: 4)
|
||
spec=str | None, # Optional path to specification file
|
||
)
|
||
```
|
||
|
||
**Parameters:**
|
||
- `design_rules`: Optional PCB design rules and constraints
|
||
- `stackup`: Optional layer stackup and materials configuration
|
||
- `num_user_layers`: Number of User.N layers to create (default: 4)
|
||
- `spec`: Optional path to a specification markdown file. Use `Path()` to resolve the path:
|
||
```python
|
||
BoardConfig(spec=Path("./docs/my-spec.md"))
|
||
```
|
||
|
||
**Design Rules Examples:**
|
||
|
||
```python
|
||
# Copper constraints (from @stdlib/board_config.zen)
|
||
Copper(
|
||
minimum_clearance=0.1, # mm
|
||
minimum_track_width=0.1, # mm
|
||
minimum_via_diameter=0.2, # mm
|
||
copper_to_edge_clearance=0.3, # mm
|
||
)
|
||
|
||
# Netclasses for different signal types
|
||
NetClass(
|
||
name="Power",
|
||
clearance=0.25, # mm
|
||
track_width=0.6, # mm
|
||
via_diameter=0.8, # mm
|
||
color="#FF0000",
|
||
)
|
||
```
|
||
|
||
**Stackup Configuration Examples:**
|
||
|
||
```python
|
||
# Material definitions (from @stdlib/board_config.zen)
|
||
Material(
|
||
name="FR-4",
|
||
vendor="Isola",
|
||
relative_permittivity=4.21, # Dielectric constant
|
||
loss_tangent=0.025, # Loss tangent
|
||
reference_frequency=1e9, # Hz
|
||
)
|
||
|
||
# Layer definitions (from @stdlib/board_config.zen)
|
||
CopperLayer(thickness=0.035, role="signal") # "signal", "power", "mixed"
|
||
DielectricLayer(thickness=1.6, material="FR-4", form="core") # "core", "prepreg"
|
||
```
|
||
|
||
**Configuration Merging (Stdlib Utilities):**
|
||
|
||
The stdlib provides utilities for merging multiple board configurations:
|
||
|
||
```python
|
||
load("@stdlib/board_config.zen", "merge_configs", "deep_merge")
|
||
|
||
base_config = BoardConfig(stackup=my_stackup)
|
||
tight_rules = BoardConfig(design_rules=tight_constraints)
|
||
|
||
# Merge multiple configs using stdlib utilities
|
||
final_config = merge_configs(base_config, tight_rules)
|
||
|
||
# Use built-in Board() with merged config
|
||
Board("Production", final_config, "layout/", default=True)
|
||
```
|
||
|
||
### TestBench(name, module, checks)
|
||
|
||
Creates a test bench for validating module connectivity and properties without requiring inputs.
|
||
|
||
```python
|
||
# Load a module to test
|
||
MyCircuit = Module("./my_circuit.zen")
|
||
|
||
# Define check functions
|
||
def verify_power_connections(module):
|
||
"""Ensure all power pins are connected to VCC"""
|
||
vcc_connections = module.nets.get("VCC", [])
|
||
check(len(vcc_connections) >= 2, "Need at least 2 VCC connections")
|
||
|
||
def verify_ground_plane(module):
|
||
"""Check for proper ground connections"""
|
||
check("GND" in module.nets, "Missing GND net")
|
||
check(len(module.nets["GND"]) > 3, "GND net needs more than 3 connections")
|
||
|
||
# Create test bench
|
||
TestBench(
|
||
name = "PowerTest",
|
||
module = MyCircuit,
|
||
checks = [verify_power_connections, verify_ground_plane]
|
||
)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `name`: String identifier for the test bench
|
||
- `module`: Module instance to test (created with `Module()`)
|
||
- `checks`: List of check functions to execute
|
||
|
||
**Check Function Signature:**
|
||
|
||
Check functions receive a single `Module` argument containing circuit data:
|
||
|
||
```python
|
||
def check_function(module: Module):
|
||
# Access circuit data through the Module
|
||
nets = module.nets # Map of net names to connected port tuples
|
||
components = module.components # Map of component paths to component objects
|
||
|
||
# Access components via indexing (new preferred method)
|
||
component = module["ComponentName"] # Direct component access
|
||
submodule = module["SubmoduleName"] # Direct submodule access
|
||
nested = module["SubmoduleName"]["ComponentName"] # Chained access
|
||
|
||
# Access component attributes
|
||
component_pins = component.pins # Component pin connections
|
||
component_type = component.type # Component type (if available)
|
||
component_props = component.properties # Component properties dict
|
||
|
||
# Perform validation - signal failures using check() or error()
|
||
check(condition, "Failure message") # Throws error if condition is False
|
||
```
|
||
|
||
**Module Contents:**
|
||
|
||
- `module.nets`: Maps each net name to a list of connected port tuples (e.g., `{"VCC": [("U1", "VDD"), ("C1", "P1")]}`)
|
||
- `module.components`: Maps component paths to component objects (e.g., `{"U1.IC": <Component>, "C1.C": <Component>}`)
|
||
- `module["name"]`: Direct indexing access to child components and submodules by name
|
||
|
||
**Check Function Behavior:**
|
||
|
||
- Check functions should use `check(condition, message)` or `error(message)` to signal failures
|
||
- Any unhandled error or exception in a check function is treated as a test failure
|
||
- Check functions do not need to return any specific value
|
||
- Use `print()` for informational output during testing
|
||
|
||
**TestBench Behavior:**
|
||
|
||
- Evaluates the module with relaxed input requirements (missing required inputs are allowed)
|
||
- Stores check functions for deferred execution (checks run later when using `pcb test` command)
|
||
- Returns a TestBench value containing test cases and deferred checks
|
||
- When executed by `pcb test`: runs each check function in order, reports failures as diagnostics with precise source location pointing to the failing `check()` call, and prints a success message if all checks pass
|
||
|
||
**Example with Modern Indexing Syntax:**
|
||
|
||
```python
|
||
def check_power_connections(module: Module):
|
||
"""Verify power IC connections using modern indexing syntax"""
|
||
# Access submodule and then component within it (chained syntax)
|
||
power_ic = module["PowerIC"]["LDO"]
|
||
|
||
# Equivalent using nested path syntax
|
||
power_ic_nested = module["PowerIC.LDO"]
|
||
|
||
# Both approaches return the same component
|
||
check(power_ic.name == power_ic_nested.name, "Chained and nested path syntax should be equivalent")
|
||
|
||
# Check that power pins are connected properly
|
||
check("VCC" in power_ic.pins["VIN"].name, "LDO input not connected to VCC")
|
||
check("GND" in power_ic.pins["GND"].name, "LDO ground not connected")
|
||
|
||
print(f"✓ {power_ic.name} power connections verified")
|
||
|
||
def check_capacitor_placement(module: Module):
|
||
"""Verify bypass capacitors are properly placed"""
|
||
# Access capacitor components by name using both syntaxes
|
||
c1 = module["C1"]["C"] # Chained syntax
|
||
c2 = module["C2.C"] # Nested path syntax
|
||
|
||
# Verify they're connected to power and ground
|
||
check("VCC" in c1.pins["P1"].name, "C1 not connected to power")
|
||
check("GND" in c1.pins["P2"].name, "C1 not connected to ground")
|
||
check("VCC" in c2.pins["P1"].name, "C2 not connected to power")
|
||
check("GND" in c2.pins["P2"].name, "C2 not connected to ground")
|
||
|
||
print(f"✓ Capacitors {c1.name} and {c2.name} properly placed")
|
||
|
||
def check_traditional_syntax(module: Module):
|
||
"""Example using traditional components dict access"""
|
||
for comp_path, component in module.components.items():
|
||
if component.type == "capacitor":
|
||
check(hasattr(component, 'capacitance'), f"{comp_path} missing capacitance")
|
||
print(f"✓ {comp_path} capacitance: {component.capacitance}")
|
||
|
||
def check_net_connectivity(module: Module):
|
||
"""Verify net connections using nets attribute"""
|
||
check("VCC" in module.nets, "Missing VCC power net")
|
||
check("GND" in module.nets, "Missing GND net")
|
||
|
||
vcc_connections = len(module.nets["VCC"])
|
||
check(vcc_connections >= 2, f"VCC only has {vcc_connections} connections, need at least 2")
|
||
|
||
print(f"✓ VCC net has {vcc_connections} connections")
|
||
|
||
def check_component_presence(module: Module):
|
||
"""Use membership operator to check component existence"""
|
||
# Check if components exist before accessing them
|
||
if "PowerIC" in module:
|
||
power_ic = module["PowerIC"]
|
||
print(f"✓ Found PowerIC submodule: {power_ic}")
|
||
|
||
# Check nested component existence
|
||
if "PowerIC.LDO" in module:
|
||
ldo = module["PowerIC.LDO"]
|
||
print(f"✓ Found LDO component: {ldo.name}")
|
||
else:
|
||
error("LDO component not found in PowerIC")
|
||
else:
|
||
print("PowerIC not present in this module")
|
||
|
||
# Check multiple components at once
|
||
required_caps = ["C1", "C2", "C3"]
|
||
missing_caps = [cap for cap in required_caps if cap not in module]
|
||
|
||
if missing_caps:
|
||
error(f"Missing required capacitors: {missing_caps}")
|
||
else:
|
||
print("✓ All required capacitors present")
|
||
|
||
TestBench(
|
||
name = "PowerCircuitValidation",
|
||
module = MyPowerCircuit,
|
||
checks = [
|
||
check_power_connections, # Uses modern indexing syntax
|
||
check_capacitor_placement, # Uses chained indexing
|
||
check_traditional_syntax, # Uses traditional components dict
|
||
check_net_connectivity, # Uses nets attribute
|
||
check_component_presence, # Uses membership operator
|
||
]
|
||
)
|
||
```
|
||
|
||
## Circuit Graph Analysis & Path Validation
|
||
|
||
### Overview
|
||
|
||
Zener provides circuit graph analysis for validating module connectivity and topology. The system converts circuit schematics into searchable graphs, enabling path finding between component pins and verification of component sequences.
|
||
|
||
The graph analysis operates on the **public interface paradigm**: path finding works between **component ports** (specific IC pins) and **external nets** (module's io() declarations), while automatically discovering internal routing paths.
|
||
|
||
### Core Concepts
|
||
|
||
#### Circuit Graph
|
||
|
||
Every module automatically generates a circuit graph that models component connectivity:
|
||
|
||
```python
|
||
def analyze_circuit(module: Module):
|
||
# Get the circuit graph from any module
|
||
graph = module.graph()
|
||
|
||
# Find paths between public interface points
|
||
paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND", max_depth=5)
|
||
```
|
||
|
||
#### Public Interface Boundaries
|
||
|
||
Path finding operates between two types of well-defined endpoints:
|
||
|
||
**Component Ports**: Specific pins on specific components
|
||
```python
|
||
("ComponentName", "PinName") # e.g., ("TPS82140", "VIN"), ("STM32", "PA0")
|
||
```
|
||
|
||
**External Nets**: Public nets exposed by the module's io() declarations
|
||
```python
|
||
"NetName" # e.g., "GND_GND", "VCC_VCC", "SPI_CLK"
|
||
```
|
||
|
||
### Path Finding API
|
||
|
||
#### graph.paths(start, end, max_depth=10)
|
||
|
||
Finds all simple paths between two points in the circuit:
|
||
|
||
```python
|
||
def validate_power_supply(module: Module):
|
||
graph = module.graph()
|
||
|
||
# IC pin to external net
|
||
input_paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND")
|
||
|
||
# IC pin to IC pin (internal feedback path)
|
||
feedback_paths = graph.paths(start=("TPS82140", "VOUT"), end=("TPS82140", "FB"))
|
||
|
||
# External net to IC pin
|
||
enable_paths = graph.paths(start="EN_EN", end=("TPS82140", "EN"))
|
||
```
|
||
|
||
**Parameters:**
|
||
- `start`: Component port tuple `("Component", "Pin")` or external net name
|
||
- `end`: Component port tuple `("Component", "Pin")` or external net name
|
||
- `max_depth`: Maximum number of components to traverse (default: 10)
|
||
|
||
**Returns:** List of Path objects
|
||
|
||
#### Path Objects
|
||
|
||
Each path contains discovered connectivity information:
|
||
|
||
```python
|
||
def analyze_path(path):
|
||
print("Path details:")
|
||
print(" Ports:", path.ports) # List of (component, pin) tuples traversed
|
||
print(" Components:", len(path.components)) # Components in the path
|
||
print(" Nets:", path.nets) # Net names traversed
|
||
```
|
||
|
||
### Path Validation Methods
|
||
|
||
#### Basic Validation
|
||
|
||
```python
|
||
# Count components matching predicate
|
||
resistor_count = path.count(is_resistor)
|
||
|
||
# Ensure at least one component matches
|
||
path.any(is_capacitor)
|
||
|
||
# Ensure all components match
|
||
path.all(is_passive_component)
|
||
|
||
# Ensure no components match
|
||
path.none(is_active_component)
|
||
```
|
||
|
||
#### Sequential Pattern Matching
|
||
|
||
The `path.matches()` method validates component sequences in order:
|
||
|
||
```python
|
||
def validate_filter_topology(module: Module):
|
||
graph = module.graph()
|
||
filter_paths = graph.paths(start=("OpAmp", "OUT"), end="GND_GND")
|
||
|
||
# Validate exact component sequence
|
||
filter_paths[0].matches(
|
||
is_resistor("1kOhm"), # Series resistor
|
||
is_capacitor("100nF"), # Filter capacitor
|
||
is_resistor("10kOhm") # Load resistor
|
||
)
|
||
```
|
||
|
||
### Design Principles
|
||
|
||
#### Datasheet Requirements Translation
|
||
|
||
Circuit validation can directly implement datasheet requirements by mapping component pin constraints to path validation:
|
||
|
||
**Power Supply Decoupling**
|
||
```python
|
||
def validate_vin_decoupling(module: Module):
|
||
"""Validate VIN decoupling per datasheet Figure 8-1"""
|
||
graph = module.graph()
|
||
|
||
# Find all paths from VIN pin to ground
|
||
vin_paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND")
|
||
|
||
# Datasheet requirement: "≥10µF bulk + 100nF ceramic"
|
||
vin_paths.any(path.matches(is_capacitor("10uF"))) # Bulk capacitor
|
||
vin_paths.any(path.matches(is_capacitor("100nF"))) # Bypass capacitor
|
||
|
||
print("✓ VIN decoupling meets datasheet requirements")
|
||
// Reference: Datasheet §8.2.1, Figure 8-1
|
||
```
|
||
|
||
**Feedback Networks**
|
||
```python
|
||
def validate_feedback_divider(module: Module):
|
||
"""Validate feedback network per datasheet Section 8.3"""
|
||
graph = module.graph()
|
||
|
||
# Find path from output to feedback pin
|
||
fb_paths = graph.paths(start=("TPS82140", "VOUT"), end=("TPS82140", "FB"))
|
||
|
||
# Datasheet requirement: "Precision resistor divider"
|
||
fb_paths[0].matches(
|
||
is_resistor(), # Upper feedback resistor
|
||
is_resistor() # Lower feedback resistor
|
||
)
|
||
|
||
print("✓ Feedback divider topology validated")
|
||
// Reference: Datasheet §8.3.2, Equation 1
|
||
```
|
||
|
||
**Bootstrap Circuits**
|
||
```python
|
||
def validate_bootstrap_cap(module: Module):
|
||
"""Validate bootstrap capacitor per datasheet Figure 8-4"""
|
||
graph = module.graph()
|
||
|
||
# Bootstrap cap connects BOOT pin to SW pin
|
||
boot_paths = graph.paths(start=("TPS82140", "BOOT"), end=("TPS82140", "SW"))
|
||
|
||
# Datasheet requirement: "100nF ceramic, X7R dielectric"
|
||
boot_paths[0].matches(is_capacitor("100nF", dielectric="X7R"))
|
||
|
||
print("✓ Bootstrap capacitor meets specifications")
|
||
// Reference: Datasheet §8.4.1, Figure 8-4
|
||
```
|
||
|
||
#### Public Interface Paradigm
|
||
|
||
**Start/End Points**: Always use the module's "public" interface:
|
||
- **Component ports**: Known IC pins from the main component
|
||
- **External nets**: Public nets from io() declarations
|
||
|
||
**Internal Discovery**: Path finding automatically traverses internal components and nets
|
||
|
||
**Benefits**:
|
||
- **Deterministic scope**: Clear rule boundaries
|
||
- **Implementation freedom**: Internal routing flexibility
|
||
- **Hierarchical composability**: Rules work at any module level
|
||
- **Performance**: Constrained search space
|
||
|
||
#### Validation Strategies
|
||
|
||
**Path Existence**: Verify required connections exist
|
||
```python
|
||
# "VIN must connect to GND through decoupling"
|
||
vin_paths = graph.paths(start=("IC", "VIN"), end="GND_GND")
|
||
check(len(vin_paths) > 0, "VIN decoupling missing")
|
||
```
|
||
|
||
**Topology Validation**: Verify exact component sequences
|
||
```python
|
||
# "Output filter must be L-C configuration"
|
||
filter_paths = graph.paths(start=("IC", "SW"), end=("IC", "VOUT"))
|
||
filter_paths[0].matches(is_inductor(), is_capacitor())
|
||
```
|
||
|
||
**Alternative Paths**: Handle multiple valid implementations
|
||
```python
|
||
# "Either RC or LC filter acceptable"
|
||
filter_paths = graph.paths(start=("IC", "OUT"), end="LOAD_LOAD")
|
||
rc_valid = any(path.matches(is_resistor(), is_capacitor()) for path in filter_paths)
|
||
lc_valid = any(path.matches(is_inductor(), is_capacitor()) for path in filter_paths)
|
||
check(rc_valid or lc_valid, "Filter topology required")
|
||
```
|
||
|
||
**Error Suppression**: Use `suppress_errors=True` for path identification
|
||
```python
|
||
# Find all paths matching a pattern without failing
|
||
matching_paths = [p for p in all_paths if p.matches(
|
||
is_resistor(), is_capacitor(), suppress_errors=True
|
||
)]
|
||
check(len(matching_paths) > 0, "No RC filter found")
|
||
```
|
||
|
||
### path.matches() API Reference
|
||
|
||
#### Syntax
|
||
|
||
```python
|
||
path.matches(*matchers, suppress_errors=False)
|
||
```
|
||
|
||
**Parameters:**
|
||
- `*matchers`: Sequential matcher functions to apply in order
|
||
- `suppress_errors`: If True, returns False on validation failure instead of raising errors
|
||
|
||
#### Sequential Processing Model
|
||
|
||
- Matchers consume components sequentially using cursor-based processing
|
||
- Each matcher receives `(path, cursor_index)` and returns components consumed
|
||
- Validation fails if any matcher fails or if components remain after all matchers
|
||
|
||
#### Built-in Matcher Functions
|
||
|
||
```python
|
||
# Basic component type matchers
|
||
is_resistor(expected_value=None) # Match resistor, optionally with specific value
|
||
is_capacitor(expected_value=None) # Match capacitor, optionally with specific value
|
||
is_inductor(expected_value=None) # Match inductor, optionally with specific value
|
||
|
||
# Navigation matchers
|
||
skip(n) # Skip exactly n components
|
||
skip_rest() # Consume all remaining components
|
||
|
||
# Quantified matchers
|
||
exactly_n_resistors(n) # Exactly n consecutive resistors
|
||
at_least_n_capacitors(n) # At least n consecutive capacitors
|
||
at_most_n_components(n) # At most n components of any type
|
||
|
||
# Conditional matching
|
||
any_of(matcher1, matcher2, ...) # Match any of the provided matchers
|
||
skip_until(matcher) # Skip components until matcher succeeds
|
||
contains_somewhere(matcher) # Matcher succeeds somewhere in remaining path
|
||
|
||
# Component properties
|
||
has_package(size) # Component with specific package size
|
||
name_contains(pattern) # Component name contains pattern
|
||
```
|
||
|
||
#### Custom Matcher Functions
|
||
|
||
```python
|
||
def custom_matcher(path, cursor):
|
||
"""Custom matcher function template"""
|
||
if cursor >= len(path.components):
|
||
error("path ended, expected component")
|
||
|
||
component = path.components[cursor]
|
||
# Validation logic here
|
||
check(component.type == "resistor", "Expected resistor")
|
||
|
||
return 1 # Consume 1 component, advance cursor
|
||
```
|
||
|
||
## Module System
|
||
|
||
### Module Definition
|
||
|
||
A module is defined by a `.zen` file that declares its inputs and creates components:
|
||
|
||
```python
|
||
# voltage_divider.zen
|
||
|
||
# Declare inputs
|
||
vin = io("vin", Net)
|
||
vout = io("vout", Net)
|
||
gnd = io("gnd", Net)
|
||
|
||
r1_value = config("r1", str, default="10k")
|
||
r2_value = config("r2", str, default="10k")
|
||
|
||
# Define a resistor symbol (could also load from library)
|
||
resistor_symbol = Symbol(
|
||
definition = [
|
||
("1", ["1"]),
|
||
("2", ["2"])
|
||
]
|
||
)
|
||
|
||
# Create components
|
||
Component(
|
||
name = "R1",
|
||
type = "resistor",
|
||
footprint = "0402",
|
||
symbol = resistor_symbol,
|
||
pins = {"1": vin, "2": vout},
|
||
properties = {"value": r1_value}
|
||
)
|
||
|
||
Component(
|
||
name = "R2",
|
||
type = "resistor",
|
||
footprint = "0402",
|
||
symbol = resistor_symbol,
|
||
pins = {"1": vout, "2": gnd},
|
||
properties = {"value": r2_value}
|
||
)
|
||
```
|
||
|
||
### Module Instantiation
|
||
|
||
```python
|
||
# Load the module
|
||
VDivider = Module("./voltage_divider.zen")
|
||
|
||
# Create instances
|
||
VDivider(
|
||
name = "divider1",
|
||
vin = Net("INPUT"),
|
||
vout = Net("OUTPUT"),
|
||
gnd = Net("GND"),
|
||
r1 = "100k",
|
||
r2 = "47k"
|
||
)
|
||
```
|