mirror of
https://github.com/diodeinc/pcb.git
synced 2026-09-14 20:17:19 +08:00
1ee83c6e7e
* Add serialize(), deserialize() builtins for debugging Signed-off-by: akhilles <akhilvelagapudi@gmail.com> * Add field(), __post_init__ to interface() Also, add better print() behavior. Signed-off-by: akhilles <akhilvelagapudi@gmail.com> * Add interface/net promotion Signed-off-by: akhilles <akhilvelagapudi@gmail.com> * Remove Cargo.toml patch Signed-off-by: akhilles <akhilvelagapudi@gmail.com> * Fix example Signed-off-by: akhilles <akhilvelagapudi@gmail.com> * Fix test workspace Signed-off-by: akhilles <akhilvelagapudi@gmail.com> * Add interface arg type checking Signed-off-by: akhilles <akhilvelagapudi@gmail.com> * Update spec.mdx Signed-off-by: akhilles <akhilvelagapudi@gmail.com> * Allow `using(Net)` Signed-off-by: akhilles <akhilvelagapudi@gmail.com> --------- Signed-off-by: akhilles <akhilvelagapudi@gmail.com>
503 lines
13 KiB
Plaintext
503 lines
13 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.
|
|
|
|
```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.
|
|
|
|
```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
|
|
)
|
|
```
|
|
|
|
**Type**: `Module`
|
|
**Constructor**: `Module(path)`
|
|
|
|
## Built-in Functions
|
|
|
|
### 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")
|
|
```
|
|
|
|
### 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)
|
|
```
|
|
|
|
## 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"
|
|
)
|
|
```
|