Claude
Skills
Sign in
Back

zener-language

Included with Lifetime
$97 forever

Canonical Zener HDL semantics and workflow. Use before reading or modifying `.zen` files. Covers module loading and instantiation, `io()`/`config()` API design, nets/interfaces/power domains, components and sourcing, `pcb.toml` manifests, stdlib/package discovery with `pcb doc`, physical units, generics, checks, DNP patterns, naming, and validation.

Design

What this skill does


# Zener Language

Canonical Zener HDL semantics and authoring guidance.

## Workflow

1. Use `pcb doc --package @stdlib` or `pcb doc --package <package>` to find the public API and source root (`<!-- source: ... -->`); add `--list` for the file tree. Read source from that root for exact behavior.
2. Preserve trailing `# pcb:sch ...` comments. Only update names inside an existing comment when you rename the matching component or net.
3. After adding, removing, or changing package `Module()` / `load()` imports, run `pcb sync` from the relevant workspace or package, then run `pcb build <path>` to validate. `pcb sync` is the dependency reconciliation step; `pcb build` is the validation step.
4. For recent Zener, stdlib, and `pcb` CLI changes, check the pcb changelog entries for the installed version and nearby previous releases: <https://github.com/diodeinc/pcb/blob/main/CHANGELOG.md>

## Language

Base language is normal Starlark — expressions, functions, loops, comprehensions, dicts, lists, `load()`. Below is the Zener-specific layer.

Modules:

- A `.zen` file is either a normal Starlark module loaded with `load()` or an instantiable schematic module loaded with `Module()`.
- `load("./foo.zen", "helper")` imports Starlark symbols. `Foo = Module("./Foo.zen")` or `Foo = Module("github.com/org/repo/path/Foo.zen")` loads a subcircuit.
- `./` paths are relative to the current file and resolve within the same package. Cross-package `load()` and `Module()` require the full package URL.
- Instantiation always passes `name=...` first, then any `io()` / `config()` inputs. Useful extras: `properties`, `dnp`, `schematic`.

Nets and interfaces:

- `Net(name=None, voltage=None, impedance=None)` is the base connection type.
- `Power`, `Ground`, and `NotConnected` are specialized net types; more specialized net types live in stdlib.
- Across `io()` boundaries: `NotConnected` can promote to any net type; specialized nets can demote to plain `Net`; plain `Net` does not auto-promote to specialized types. Use explicit casts like `Power(net, voltage=...)` or `Net(power_net)` when needed.

Components and sourcing:

- `Component(...)` is the primitive physical-part constructor. Required fields are effectively `name`, `symbol`, and `pins`.
- The symbol is the source of truth for footprint, part metadata, and datasheet metadata. Make the symbol properties correct; do not repeat `footprint=`, `part=`, or `datasheet=` in `Component()` when they are already provided by the symbol.
- Prefer `part=Part(mpn=..., manufacturer=...)` over legacy scalar `mpn` and `manufacturer` when part metadata is not already in the symbol.
- `Symbol(library, name=None)` points at a `.kicad_sym`; `name` is required for multi-symbol libraries.
- Omit `no_connect` pins from `pins`; `Component()` wires `NotConnected()` automatically.

`io()`:

- Preferred form: flat top-level `NAME = io(template, ...)` where `template` is a net/interface type or instance, e.g. `Power(voltage="3.3V")`.
- Do not introduce `Pins = struct(...)` wrappers for component pins; that older style is deprecated. Existing packages may still use it, but new and touched `.zen` should expose pins as top-level `io()`s.
- Name is inferred from the assignment target. `optional=True` means omitted inputs get auto-generated nets or interfaces.

`config()`:

- Preferred form: `name = config(typ, default=..., ...)`; name is inferred from the assignment target.
- `typ` can be primitive types, enums, records, or physical values such as `Voltage`, `Current`, or `Resistance`.
- Use physical types from `@stdlib/units.zen` for every physical-value config, even when only a few choices are valid. Constrain discrete choices with `allowed=[...]`; strings auto-convert, e.g. `config(Current, default="3A", allowed=["1A", "2A", "3A"])`.
- Use `enum()` only for non-physical design choices such as operating mode, protocol variant, polarity, or enablement strategy.

Public compatibility:

- For reusable packages, compatibility means existing consumers can update without changing their Zener, layout, or integration assumptions.
- Breaking changes include public interface changes (`io()`, `config()`, entrypoints, module call shape), substantial layout/physical integration changes, or behavior changes that require consumer action. Collapsing loose ios into one interface is breaking even if the netlist still builds.
- `pcb build` passing only validates the current package; it does not prove existing consumers remain compatible. When making a breaking change, document the migration and mark the commit as breaking.

Utilities:

- `Layout(name, path)` associates reusable layout metadata to a module.
- `check(condition, message)`, `warn(message)`, and `error(message)` are the validation and diagnostic primitives.

## Authoring Idioms

### Power, Interfaces, And Checks

- Keep rails explicit with prelude `Power(voltage=...)` and `Ground`; each public `Power` `io()` declares its voltage range unless the local API intentionally keeps it generic.
- Use `@stdlib/interfaces.zen` interfaces for buses and grouped signals that are not in the prelude; prefer public bus interfaces such as `I2c`, `Spi`, `Qspi`, `Uart`, `Usb2`, or `DiffPair` over separate loose top-level nets when the grouped signal semantics are clear.
- Use typed values and validation primitives (`check(...)`, `warn(...)`, `error(...)`, `@stdlib/checks.zen`) for electrical constraints instead of comments when possible.
- Connect `Power` and `Ground` ios directly to pins and passives.

```zen
VDD = io(Power(voltage="3.0V to 5.5V"))
GND = io(Ground)
EN = io(Net, help="High to enable the regulator")
```

### Configs And Computation

- Expose meaningful design choices, not incidental implementation details. Good configs include output voltage, gain, cutoff frequency, address, mode, or optional feature enablement. Avoid configs for fixed decoupling values, passive package sizes, and test-point style unless local code already makes them public API.
- Prefer one meaningful physical config over raw R/C/L strings. For example, expose a cutoff `Frequency` and compute snapped passives internally.
- Put non-trivial calculations in named functions with datasheet section or equation references when available. Snap results to E-series values with `e96()`, `e24()`, or the appropriate stdlib utility.

```zen
def load_r(v_out, v_sense):
    """Datasheet §8.1.1 / Eq 4: V_OUT = V_SENSE × gm × R_L"""
    GM = Current("200uA") / Voltage("1V")
    return e96(v_out / (v_sense * GM))
```

### DNP And Optional Circuitry

- Configs may change component values and `dnp=` state, but they should not change which instances or nets exist in the schematic.
- Never use conditional instantiation to add, remove, or reconnect circuitry. Always instantiate the relevant components and use `dnp=` for population state.
- When a config selects a value on the same two nets, prefer one component with a computed value.
- When a config selects between mutually exclusive net straps, instantiate each strap option and DNP the inactive ones so topology stays stable.
- Leverage an IC's internal pull-up or pull-down when the default mode uses it; use external bias components with `dnp=` only for populated alternatives.

```zen
load("@stdlib/units.zen", "Voltage", "Resistance")
load("@stdlib/utils.zen", "e96")

Resistor = Module("@stdlib/generics/Resistor.zen")

Mode = enum("PFM", "PWM")
mode = config(Mode, default="PFM")
voltage_out = config(Voltage, default="5V", allowed=["3.3V", "5V"])

VOUT = io(Power(voltage=voltage_out))
GND = io(Ground())

VFB_REF = Voltage("0.8V")
R_FB_TOP_VAL = Resistance("100kohm")

def fb_bottom(vout):
    """Datasheet Table 1: R2 = R1 × VFB / (VOUT − VFB)"""
    return e96(R_FB_TOP_VAL * VFB_REF / (vout - VFB_REF))

VCC = Power()
FB = Net()
MSYNC = Net()

# Same feedback divider instances and nets for every output voltage; only value changes.
Resistor(name="R_FB_TOP", value=R_FB_TOP_VAL.with_tolerance("1%"), package="0402", P1=VOUT
Files: 1
Size: 13.9 KB
Complexity: 19/100
Category: Design

Related in Design