Files
akhilles c680fa9c52 Update TestBench, module docs
Signed-off-by: akhilles <akhilvelagapudi@gmail.com>
2025-09-29 17:36:50 -04:00

1367 lines
43 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 - these are aliases to builtin physical unit types
load("@stdlib/units.zen", "Voltage", "Resistance") # Same as builtin.Voltage, builtin.Resistance
# Load from KiCad symbols library
R_symbol = Symbol(library = "@kicad-symbols/Device.kicad_sym", name = "R")
```
**Note**: The `@stdlib/units.zen` module provides convenient aliases to the language's builtin physical unit types. When you import symbols like `Voltage`, `Resistance`, etc. from `units.zen`, you're actually getting references to `builtin.Voltage`, `builtin.Resistance`, etc. The stdlib unit symbols are automatically replaced with their native builtin counterparts during module evaluation for optimal performance.
#### 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.
```python
# Create a net with optional name
net1 = Net()
net2 = Net("VCC")
```
**Type**: `Net`
**Constructor**: `Net(name="")`
- `name` (optional): String identifier for the net
### 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, type validation, and promotion semantics.
#### 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)
```
**using() Specifications**: Mark fields as promotion targets for automatic type conversion
```python
NET = using(Net("VCC"))
uart = using(Uart())
```
#### 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 = using(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 = using(Power()),
uart = using(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)
```
#### Promotion Semantics
Fields marked with `using()` enable automatic type promotion when passing interfaces across module boundaries:
**Rules:**
- **Unique promotion targets**: Only one `using()` field per type per interface - duplicate promotion targets to the same type are not allowed
- **Cross-module promotion**: Automatic conversion when crossing module boundaries
- **Same-module access**: Explicit field access required within same module
- **Type safety**: Promotion only occurs when target type exactly matches the expected type
```python
# Valid: Single promotion target per type
Power = interface(
NET = using(Net("VCC")), # Only Net promotion target
voltage = field(Voltage, ...),
)
# Invalid: Multiple promotion targets for same type
InvalidInterface = interface(
NET1 = using(Net("VCC")), # Error: duplicate Net promotion target
NET2 = using(Net("GND")), # Error: duplicate Net promotion target
)
# Automatic promotion when passed to modules
Resistor("R1", "10kOhm", "0603", P1=power, P2=gnd) # power promotes to power.NET
# Explicit access within same module
net = power.NET # Must use .NET explicitly
```
#### Post-Initialization Callbacks
```python
def _power_post_init(self):
if self.voltage.value <= 0:
error("Power voltage must be positive")
Power = interface(
NET = using(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, `field()` specifications, or `using()` 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 Methods**:
- `module.nets()` - Returns dict mapping net names to lists of connected port tuples
- `module.components()` - Returns dict mapping component paths to component objects
- `module.graph()` - Returns CircuitGraph object for path finding and topology validation
**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
### PhysicalValue
Physical values represent electrical quantities with units and tolerances, enabling unit-aware arithmetic and dimensional analysis.
```python
# Create physical values using unit type constructors
voltage = Voltage("3.3V")
resistance = Resistance("10kOhm")
capacitance = Capacitance("100nF")
# Unit-aware arithmetic
power = voltage * voltage / resistance # Result: Power type
time_constant = resistance * capacitance # Result: Time type
# Convert units and add tolerances
voltage_mv = voltage.with_unit("mV") # Convert to millivolts
resistor = resistance.with_tolerance("5%") # Add 5% tolerance
updated = voltage.with_value(5) # Change to 5V, keep unit and tolerance
# Parse from strings with automatic unit detection
freq = Frequency("100MHz")
temp = Temperature("25°C")
# Single argument constructors
voltage_5v = Voltage(5) # 5V (numeric)
resistance_1k = Resistance("1k") # 1kΩ (string with unit)
# Dimensionless values and casting
dimensionless = voltage.with_unit(None) # Remove units
new_current = Current(dimensionless) # Cast dimensionless to current
# Comparisons
check(voltage > Voltage("2V"), "Voltage too low") # Compare with string
check(resistance == "10kOhm", "Wrong resistance") # Equality with string
sorted_voltages = sorted([v1, v2, v3]) # Sort physical values
```
**Type**: `PhysicalValue`
**Available Unit Types**:
- `Voltage` - electrical potential (V, mV, kV)
- `Current` - electrical current (A, mA, µA)
- `Resistance` - electrical resistance (Ω, kΩ, MΩ)
- `Capacitance` - electrical capacitance (F, µF, nF, pF)
- `Inductance` - electrical inductance (H, mH, µH)
- `Frequency` - frequency (Hz, kHz, MHz, GHz)
- `Time` - time duration (s, ms, µs, ns)
- `Temperature` - temperature (°C, °F, K)
- `Charge` - electrical charge (C, mC, µC)
- `Power` - electrical power (W, mW, µW)
- `Energy` - energy (J, Wh, kWh)
- `Conductance` - electrical conductance (S)
- `MagneticFlux` - magnetic flux (Wb)
**Constructor**: `UnitType(value, tolerance=None)` or `UnitType(value_only)`
- `value`: String with value and unit (e.g., "3.3V", "10kΩ"), numeric value, or existing PhysicalValue
- `tolerance`: Optional tolerance specification (e.g., "5%", "±0.1V")
**Single Argument Constructor**:
- `UnitType(5)` - Creates value with numeric 5 in the unit type (e.g., `Voltage(5)` = 5V)
- `UnitType("5V")` - Parses string with unit (e.g., `Voltage("5V")` = 5V)
- `UnitType(other_physical_value)` - Converts between compatible units or casts dimensionless values
**Methods**:
- `with_unit(unit)` - Convert to specified unit, returns new PhysicalValue
- `with_unit(None)` - Convert to dimensionless value, returns new PhysicalValue
- `with_tolerance(tolerance)` - Add/modify tolerance, returns new PhysicalValue
- `with_value(value)` - Update the numeric value while preserving unit and tolerance, returns new PhysicalValue
**Unit-Aware Operations**:
- Arithmetic operations automatically handle unit conversions and dimensional analysis
- Compatible units are automatically converted (e.g., V + mV = V)
- Dimensional analysis creates appropriate result types (e.g., V/A = Ω, R×C = s)
- Supports all electrical relationships: Ohm's law (V=I×R), power (P=V×I), time constants (τ=RC), etc.
- Invalid operations (e.g., V + Hz) produce dimensionless results
- Physical values can be mixed with strings and numbers in arithmetic operations
**Comparisons**:
- Physical values support equality (`==`, `!=`) and ordering (`<`, `>`, `<=`, `>=`) operations
- Units must match for comparisons, except dimensionless values can compare with any unit
- Tolerance is ignored in equality comparisons - only value and unit matter
- Can compare with strings (e.g., `voltage == "5V"`) if they represent compatible physical values
**Dimensionless Values**:
- Dimensionless physical values can be cast to any unit type
- Use `with_unit(None)` to convert any physical value to dimensionless
- Dimensionless results are produced by unit cancellation (e.g., R×G = 1, V/V = 1)
- Numeric literals are treated as dimensionless when mixed with physical values
**String Representation**:
Physical values maintain lossless string conversion, preserving original units and tolerances.
### MetadataContainer
Metadata containers provide typed, persistent storage that works across module boundaries. Values are validated against the container's declared type and can be accumulated over time.
```python
# Create typed metadata containers
voltage_log = metadata(Voltage)
error_log = metadata(str)
measurements = metadata(float)
# Store values with type checking
voltage_log.push(Voltage("3.3V")) # Valid
voltage_log.push("invalid") # Error: type mismatch
# Retrieve values
latest = voltage_log.get() # Most recent value or None
all_values = voltage_log.list() # All values as list
```
**Type**: `MetadataContainer`
**Created by**: `metadata()` function
**Methods**:
- `push(value)` - Add a value (must match container's type)
- `get()` - Get the most recent value, or `None` if empty
- `list()` - Get all values as a list in chronological order
**Supported Types**:
- Basic types: `str`, `int`, `float`, `bool`, `list`, `dict`
- Physical values: `Voltage`, `Current`, `Resistance`, etc.
- Not supported: enums, records, custom types
### 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
## Built-in Functions
### metadata(type)
Creates a typed metadata container for accumulating values across module boundaries.
**Parameters**:
- `type`: Type constructor (e.g., `str`, `Voltage`, `float`)
**Returns**: `MetadataContainer` with the specified type
```python
# Create containers for different types
voltage_log = metadata(Voltage)
message_log = metadata(str)
readings = metadata(float)
# Use the container methods
voltage_log.push(Voltage("5V"))
latest_reading = voltage_log.get()
all_readings = voltage_log.list()
```
**Supported Types**: Basic types (`str`, `int`, `float`, `bool`, `list`, `dict`) and PhysicalValue types. Enums and records are not supported.
### 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, 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"))
```
**Parameters:**
- `name`: String identifier for the input
- `type`: Expected type (`Net` or interface type)
- `default`: Default value if not provided by parent
- `optional`: If True, returns None when not provided (unless default is specified)
### 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.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.
## 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, config, layout_path, 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")
# Define board 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 stdlib function
Board(
name="MyBoard",
config=config,
layout_path="layout/",
default=True
)
```
**Parameters:**
- `name`: String identifier for the board configuration
- `config`: `BoardConfig` record containing design rules and stackup
- `layout_path`: Directory path where layout files will be generated
- `layout_hints`: Optional list of layout optimization hints
- `default`: If True, this becomes the default board config for the project
**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
)
```
**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, test_cases, checks)
Creates a test bench for validating module behavior across multiple test cases with parametric inputs.
```python
# Load a module to test
MyCircuit = Module("./my_circuit.zen")
# Define check functions that receive (module, inputs)
def verify_power_connections(module, inputs):
"""Ensure all power pins are connected to VCC"""
nets = module.nets()
vcc_connections = nets.get("VCC", [])
check(len(vcc_connections) >= 2, "Need at least 2 VCC connections")
def verify_ground_plane(module, inputs):
"""Check for proper ground connections"""
nets = module.nets()
check("GND" in nets, "Missing GND net")
check(len(nets["GND"]) > 3, "GND net needs more than 3 connections")
# Create test bench with multiple test cases
TestBench(
name = "PowerTest",
module = MyCircuit,
test_cases = {
"case_3v3": {"voltage": "3.3V", "current": "1A"},
"case_5v": {"voltage": "5V", "current": "2A"},
},
checks = [verify_power_connections, verify_ground_plane]
)
```
**Parameters:**
- `name`: String identifier for the test bench
- `module`: ModuleLoader to test (created with `Module()`)
- `test_cases`: Dict mapping case names to input parameter dicts
- `checks`: Optional list of check functions to execute for each test case
**Check Function Signature:**
Check functions receive two arguments: the evaluated module and the test case inputs:
```python
def check_function(module: Module, inputs: dict):
# Access circuit data by calling Module methods
nets = module.nets() # Returns dict of net connections
components = module.components() # Returns dict of component objects
graph = module.graph() # Returns CircuitGraph object
# Access test case parameters
voltage = inputs["voltage"]
current = inputs["current"]
# Access components via indexing
component = module["ComponentName"]
submodule = module["SubmoduleName"]
nested = module["SubmoduleName"]["ComponentName"]
# Perform validation - signal failures using check() or error()
check(condition, "Failure message")
```
**Module Methods:**
- `module.nets()` - Returns dict mapping net names to lists of connected port tuples
- Example: `{"VCC": [("U1", "VDD"), ("C1", "P1")]}`
- `module.components()` - Returns dict mapping component paths to component objects
- Example: `{"U1.IC": <Component>, "C1.C": <Component>}`
- `module.graph()` - Returns CircuitGraph for path finding and topology validation
- `module[name]` - Direct indexing access to child components and submodules
**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 once for each test case with the provided inputs
- Executes all check functions for each successful module evaluation
- Reports failures as diagnostics with precise source location
- Prints a summary message showing passed/failed checks across all test cases
- Returns a TestBench value containing results
**Example with Multiple Test Cases:**
```python
MyModule = Module("./voltage_regulator.zen")
def check_output_voltage(module, inputs):
"""Verify output voltage configuration"""
components = module.components()
regulator = components.get("U1.REG")
expected_voltage = inputs["output_voltage"]
check(regulator is not None, "Regulator component not found")
print(f"✓ Output voltage configured for {expected_voltage}")
def check_bypass_capacitors(module, inputs):
"""Verify proper decoupling"""
graph = module.graph()
# Find paths from VIN to GND through capacitors
vin_paths = graph.paths(start=("U1", "VIN"), end="GND_GND")
check(len(vin_paths) > 0, "No bypass capacitor found on VIN")
print(f"✓ Found {len(vin_paths)} bypass capacitor paths")
def check_component_existence(module, inputs):
"""Verify required components are present"""
if "U1" in module:
regulator = module["U1"]
print(f"✓ Found regulator: {regulator}")
else:
error("Regulator not found")
TestBench(
name = "VoltageRegulatorTest",
module = MyModule,
test_cases = {
"3v3_config": {
"output_voltage": "3.3V",
"input_voltage": "5V",
},
"5v_config": {
"output_voltage": "5V",
"input_voltage": "12V",
},
},
checks = [
check_output_voltage,
check_bypass_capacitors,
check_component_existence,
]
)
```
**Named Check Functions:**
You can provide custom names for check functions using tuples:
```python
TestBench(
name = "MyTest",
module = MyModule,
test_cases = {"default": {}},
checks = [
("Power Check", check_power),
("Ground Check", check_ground),
]
)
```
## 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, inputs):
# Get the circuit graph from the 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, inputs):
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, inputs):
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, inputs):
"""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, inputs):
"""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, inputs):
"""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"
)
```