# Antenna matching network Source: https://docs.pcb.new/pages/antenna_matching_network Build a reusable RF matching network with modern Zener packages This guide builds an antenna signal path from three Zener elements: * `Component()` wrappers for the connector and antenna * a reusable matching-network module * a board that connects the complete RF path The example uses the repository model defined in the [language specification](/pages/spec): the board calls `Board()`, reusable blocks live under `modules/` and `components/`, and local modules use relative paths. Rendered antenna matching board ## Repository layout Create one package for each reusable block: ```text theme={null} my-rf-demo/ ├── pcb.toml ├── AntennaDemo.zen ├── modules/ │ └── AntennaMatch/ │ ├── AntennaMatch.zen │ └── pcb.toml └── components/ ├── ChipAntenna/ │ ├── ChipAntenna.zen │ ├── ChipAntenna.kicad_sym │ ├── ChipAntenna.kicad_mod │ └── pcb.toml └── SMAConnector/ ├── SMAConnector.zen ├── SMAConnector.kicad_sym ├── SMAConnector.kicad_mod └── pcb.toml ``` The root `pcb.toml` can be as small as: ```toml theme={null} [workspace] pcb-version = "0.4" [board] name = "AntennaDemo" path = "AntennaDemo.zen" description = "Replace with concise board description." ``` Use `pcb new board` and `pcb new package` to create this structure. The component values below are examples. Select production values from the antenna data, finished PCB stackup, and vector network analyzer measurements. Before writing Zener source, copy the KiCad symbol and footprint for each physical part into its package directory with the filenames shown above. Zener does not copy these assets from an installed KiCad library. Confirm that the symbol pin names match the wrapper's `pins` mapping. ## 1. Wrap the physical endpoints Use standard-library generics for common passives. Use a small custom module for the specific SMA connector and antenna. Each wrapper calls `Component()` once and exposes the required pins through `io()`. ### SMA connector ```python theme={null} # components/SMAConnector/SMAConnector.zen CENTER = io(Net, help="Coax center conductor") SHIELD = io(Ground, help="Coax shield") prefix = config(str, default="J") Component( name = "SMAConnector", symbol = Symbol( library = "SMAConnector.kicad_sym", ), footprint = File("SMAConnector.kicad_mod"), prefix = prefix, pins = { "In": CENTER, "Ext": SHIELD, }, ) ``` ### Chip antenna ```python theme={null} # components/ChipAntenna/ChipAntenna.zen FEED = io(Net) prefix = config(str, default="AE") Component( name = "ChipAntenna", symbol = Symbol( library = "ChipAntenna.kicad_sym", ), footprint = File("ChipAntenna.kicad_mod"), prefix = prefix, pins = { "A": FEED, }, ) ``` Use raw `Component()` only when a standard-library generic does not represent the physical part. Standard-library generics provide maintained symbols, footprints, and BOM behavior for the matching components. Hover over `pins` in the editor to inspect the pin names loaded from the symbol. ## 2. Build the matching module The module reserves three tuning footprints: a series inductor, a shunt capacitor, and a final series resistor. Populate the resistor with a damping value or with `0ohm`. ```python theme={null} # modules/AntennaMatch/AntennaMatch.zen load("@stdlib/units.zen", "Capacitance", "Inductance", "Resistance") Capacitor = Module("@stdlib/generics/Capacitor.zen") Inductor = Module("@stdlib/generics/Inductor.zen") Resistor = Module("@stdlib/generics/Resistor.zen") series_l = config(Inductance, default=Inductance("6.8nH")) shunt_c = config(Capacitance, default=Capacitance("1.5pF")) series_r = config(Resistance, default=Resistance("0ohm")) package = config(str, default="0402") RF_IN = io(Net, direction="input") RF_OUT = io(Net, direction="output") GND = io(Ground) MATCH_NODE = Net() Inductor( name = "L_SERIES", value = series_l, package = package, P1 = RF_IN, P2 = MATCH_NODE, ) Capacitor( name = "C_SHUNT", value = shunt_c, package = package, P1 = MATCH_NODE, P2 = GND, ) Resistor( name = "R_SERIES", value = series_r, package = package, P1 = MATCH_NODE, P2 = RF_OUT, ) Layout(name = "AntennaMatch", path = "layout/AntennaMatch") ``` Use `io()` for parent connections and `Net()` for internal nodes such as `MATCH_NODE`. The declared `config()` types convert parent-supplied strings such as `"6.8nH"`, `"1.5pF"`, and `"0ohm"` to physical quantities. ## 3. Compose the board Instantiate the connector, matching network, and antenna in the board file: ```python theme={null} # AntennaDemo.zen SMAConnector = Module("./components/SMAConnector/SMAConnector.zen") ChipAntenna = Module("./components/ChipAntenna/ChipAntenna.zen") AntennaMatch = Module("./modules/AntennaMatch/AntennaMatch.zen") rf_source = Net("RF_SOURCE") rf_feed = Net("RF_FEED") gnd = Ground("GND") SMAConnector( name = "J_RF", CENTER = rf_source, SHIELD = gnd, ) AntennaMatch( name = "MATCH", RF_IN = rf_source, RF_OUT = rf_feed, GND = gnd, series_l = "6.8nH", shunt_c = "1.5pF", series_r = "0ohm", package = "0402", schematic = "embed", ) ChipAntenna( name = "AE1", FEED = rf_feed, ) Board( name = "AntennaDemo", layers = 2, layout_path = "layout/AntennaDemo", ) ``` The resulting RF path is: ```text theme={null} SMA center -> series inductor -> tuning node -> series resistor -> antenna feed | shunt capacitor | ground ``` ## 4. Build and lay out the board Run these commands from the repository root: ```bash theme={null} pcb build AntennaDemo.zen pcb layout AntennaDemo.zen ``` `pcb build` validates the design. `pcb layout` generates the KiCad board and opens it for placement and routing. RF performance depends on placement and stackup. Place the matching components close to the antenna feed, route the RF trace for the required impedance, and leave access for component changes during bringup. ## Optional mounting holes Add mounting holes with the standard-library generic: ```python theme={null} MountingHole = Module("@stdlib/generics/MountingHole.zen") for i in range(4): MountingHole( name = "H" + str(i + 1), diameter = "M2", ) ``` Run `pcb layout` again, then place the holes to satisfy the enclosure and copper keep-out requirements. ## Design rationale * The antenna and connector packages contain only physical-part definitions. * The matching network owns the topology and tuning values. * The board file contains only board-level composition. This separation permits changes to matching values, footprints, or the antenna package without changing board-level connectivity. # Bringup documentation Source: https://docs.pcb.new/pages/docs_bringup Write board bringup and rework instructions for Diode Glass # Bringup documentation Create one `README.md` for each supported board version. Document required rework, firmware installation, and verification procedures. Diode Glass renders this file in the **Bringup** tab. ## File location ``` bringup/ v1.0/ README.md images/ estop-rework.png v2.0/ README.md ``` Store images in the `images/` directory beside the version README. ### Version matching | Release | Directory | Result | | -------- | --------- | --------------------------------------- | | `v2.0` | `v2.0/` | Exact match | | `v2.0.4` | `v2.0/` | Prefix match for the same minor version | | `v2.1.0` | `v2.0/` | No match | Create a directory for each minor version. Patch releases share that directory unless a patch requires different instructions. Diode Glass reads the documentation from `main`, not from the release archive. ## Document structure ```markdown theme={null} # Bringup Overview text. ## Reworks ### 1. Title of first rework ... ### 2. Title of second rework ... ## Firmware Free-form markdown. ## Testing Free-form markdown. ``` Diode Glass parses `## Reworks` into structured items. It renders other level-two sections as collapsible Markdown. ## Rework items Start each rework with a `### N. Title` heading. State the observed problem, the required correction, its board location, and the version in which the design was fixed. Do not encode status in a separate field. ### Link to components Use the `pcb://` scheme for reference designators, for example `[R1](pcb://R1)`. Diode Glass links these references to the PCB viewer. ### Add images Store each image under `./images/` and use descriptive alternative text: `![Reworked ESTOP resistor](./images/estop-rework.png)`. ## Example ```markdown theme={null} # DM0001 v1.0 Bringup Board version v1.0 requires three reworks before bringup. Items 1–2 are fixed in v1.1. Item 3 requires a physical fix on each board. ## Reworks ### 1. Short ESTOP debounce resistor (fixed in v1.1) The 47 kohm ESTOP debounce resistor [R_ESTOP_DEBOUNCE](pcb://R_ESTOP_DEBOUNCE) forms a voltage divider with the ESTOP LED. The divider produces approximately 1.5 V, below the AND gate's 2.0 V input-high threshold. Bridge the resistor with solder or wire. It is an 0402 part beside the ESTOP connector, between the `ESTOP_RAW` and `ESTOP` nets. ![Bridged ESTOP debounce resistor](./images/estop-rework.png) ### 2. Replace decoupling capacitors (fixed in v1.1) [C5](pcb://C5) and [C6](pcb://C6) provide insufficient 10 uF decoupling for the [U1](pcb://U1) LDO. Replace both with 22 uF, 16 V X5R capacitors, manufacturer part number `GRM188R61C226ME15`. Both parts are on the bottom side beside [U1](pcb://U1). ### 3. CAN termination resistor The [R12](pcb://R12) footprint is unpopulated. Install a 120 ohm 0402 resistor to terminate the CAN bus. ## Firmware ### Prerequisites - J-Link programmer - Firmware v1.0.2: `dm0001-v1.0.2.bin` ### Flash procedure 1. Connect the J-Link programmer to SWD header [J5](pcb://J5). 2. Apply 12 V power to the board. 3. Run `JFlash -openprj dm0001.jflash -open dm0001-v1.0.2.bin -auto -exit`. 4. Confirm that [D1](pcb://D1) blinks at 1 Hz. ## Testing ### Power rail verification | Rail | Test Point | Expected | Tolerance | |------|-----------|----------|-----------| | 3.3 V | [TP1](pcb://TP1) | 3.30 V | ±50 mV | | 1.8 V | [TP2](pcb://TP2) | 1.80 V | ±30 mV | | 5.0 V | [TP3](pcb://TP3) | 5.00 V | ±100 mV | ### CAN bus test 1. Connect a CAN analyzer to [J2](pcb://J2). 2. Send a frame with identifier `0x100`, DLC 8, and payload `0xFF`. 3. Confirm that the board responds on identifier `0x101` within 10 ms. ### ESTOP functional test 1. Short ESTOP connector pins 1 and 2. 2. Confirm that the `ESTOP_OUT` test point measures 3.3 V after rework 1. 3. Remove the short from the ESTOP connector. 4. Confirm that `ESTOP_OUT` measures 0 V. ``` # README conventions Source: https://docs.pcb.new/pages/docs_readme Write practical README files for boards and reusable packages # README conventions Every board and package must have a `README.md` in its root directory. The README must identify the artifact, explain its purpose, and give a technically competent reader enough information to build or use it. Keep the document current with the source. Remove empty sections, placeholders, historical plans, and implementation details that do not affect the reader. ## Board README A board README serves engineers who build, manufacture, flash, or test the board. Include its purpose, main devices, key interfaces, operating limits, and the exact build commands. State any required tools or external assets before the procedure. Use this structure: ````markdown theme={null} # Describe the board, its main function, and its important operating limits. ## Features - List the main controller or processor. - List the external interfaces. - List significant power or safety features. ## Build State nonstandard prerequisites, then run: ```bash pcb build .zen pcb layout .zen ``` Describe the generated output and any expected warnings. ## License State the applicable license. ```` Add pin assignments, design constraints, firmware instructions, or related datasheets only when they are required to use the board. ## Package README A package README serves engineers who import a reusable component or module. Identify every public symbol and provide one complete import or instantiation example. Use this structure: ````markdown theme={null} # Describe what the package exports and when to use it. ## Usage ```python load(".zen", "ExportedSymbol") ``` ## Exports | Symbol | Description | |---|---| | `ExportedSymbol` | State its function and important limits. | ## Configuration ```python instance = ExportedSymbol( required_input = value, ) ``` ```` Document required inputs, defaults, units, limitations, and layout constraints where they affect correct use. Omit the configuration section if the package has no configurable behavior. ## Style * Start with purpose and scope. Define a term before using it. * Use active voice and direct instructions. Use the same term for the same concept throughout the document. * Put identifiers, file names, commands, and pin names in backticks. * Use numbered steps for procedures and bullets only for short collections. * Include complete commands and examples. State prerequisites, output, side effects, and common failure conditions when relevant. * Delete redundant prose, speculative plans, and sections with no reader-facing content. # Packages Source: https://docs.pcb.new/pages/packages Package management, workspaces, and dependency resolution Package versions identify immutable source snapshots. A dependency on `component-lib@0.3.2` selects the same source for every build until the manifest changes. ## Version policy PCB packages use semantic versions with hardware-specific compatibility rules: | Change | Allowed contents | | --------------------------------- | -------------------------------------------------------------------------------------------------- | | Patch, such as `0.3.1` to `0.3.2` | Documentation and metadata changes that do not alter connectivity, layout, or electrical behavior. | | Minor, such as `0.3` to `0.4` | New compatible behavior after `1.0`; a breaking compatibility lane before `1.0`. | | Major, such as `1.x` to `2.0` | Breaking changes such as changed pins, interfaces, or removed behavior. | Per the [Semantic Versioning specification](https://semver.org/#spec-item-4), pre-1.0 packages have no stable public API. PCB therefore treats `0.3.x` and `0.4.x` as separate compatibility families. Versions within one family must remain compatible: ``` v0.3.x family: 0.3.0, 0.3.1, 0.3.2, ... (compatible) v0.4.x family: 0.4.0, 0.4.1, ... (compatible) v0.3.x and v0.4.x: different families (potentially incompatible) ``` When a dependency graph requires several versions from one family, resolution selects the highest required version in that family. Authors must not publish a breaking change within a family. PCB combines Minimal Version Selection (MVS) with hydrated `pcb.toml` manifests for reproducible builds. `pcb sync` records the selected dependency graph, including immutable pseudo-versions for branch and commit dependencies. Build commands then reuse that recorded graph without selecting newly published versions. ## Workspace package discovery Starting at the workspace root, `pcb` searches at most eight directory levels and treats each descendant directory containing `pcb.toml` as a package. `[workspace].exclude` controls discovery: ```toml theme={null} [workspace] pcb-version = "0.4" exclude = ["scratch/**", "experiments/old-board"] ``` `pcb` does not search inside an excluded directory. It also skips generated and cache directories such as `.git`, `.pcb`, `vendor`, `target`, `node_modules`, and `fork`. ## Coexisting versions A build can contain multiple incompatible families of one package. For example: ```toml theme={null} # Library X [dependencies] "github.com/acme/component-lib" = "0.3" # Library Y [dependencies] "github.com/acme/component-lib" = "1.0" ``` The resolver retains separate copies of `component-lib@0.3.x` and `component-lib@1.x` for their respective dependents. This permits incremental migration and diamond dependencies. Values from the two families have distinct types and cannot be passed across the compatibility boundary. ## Minimal Version Selection PCB uses MVS, based on [Go modules](https://go.dev/ref/mod). For each package family, MVS selects the lowest version that satisfies every explicit minimum in the dependency graph. Newly published versions do not change the result unless a manifest requires them. ### How MVS works Consider this dependency graph: ``` Board ├── component-lib >= 0.3 └── regulator >= 1.0 └── component-lib >= 0.3.2 ``` The board requires `component-lib >= 0.3.0`, while `regulator` requires `component-lib >= 0.3.2`. MVS therefore selects `0.3.2`, even if `0.3.9` exists. Require a newer version explicitly when the project is ready to test it: ```toml theme={null} [dependencies] "github.com/acme/component-lib" = "0.3.9" ``` MVS is deterministic and does not backtrack. Within each compatibility family, the selected version is the highest minimum requested by any dependent. ### Resolution algorithm 1. **Seed:** Collect direct dependencies from all workspace packages. Group by package path and compatibility family. Initialize each family to the highest version explicitly required. 2. **Discover:** Fetch manifests for selected versions. For each transitive dependency, if it requires a higher version within an existing family, upgrade. Repeat until the selected graph no longer changes. 3. **Build closure:** Trace the dependency graph from workspace roots using final versions. This filters out any versions that were superseded during discovery. For multiple compatibility families: ``` WV0001: component-lib = 0.2.13 WV0002: component-lib = 0.3.2, regulator = 1.0 WV0003: component-lib = 0.3.1 regulator@1.0.0: component-lib = 0.3.0 Result: v0.2.x family → component-lib@0.2.13 v0.3.x family → component-lib@0.3.2 (max of 0.3.2, 0.3.1, 0.3.0) ``` Both versions remain in the build because they belong to different families. ## Import paths as identity Import paths serve as globally unique package identifiers: ```python theme={null} load("@stdlib/units.zen", "Voltage") load("github.com/myorg/components/capacitor.zen", "Capacitor") ``` The path identifies the package owner and source repository without a central namespace. A file's imports also state which packages its source requires. Import paths omit versions. The `pcb.toml` manifest declares which version of each package to use: ```toml theme={null} [dependencies] "code.diode.computer/diode/registry/components/ti/tps54331" = "1.0" ``` This separation keeps import statements stable across upgrades and confines version changes to manifests. The same source file can use different selected versions in different workspaces. ## Hydrated manifests Workspaces store resolved dependency state in `pcb.toml`. `pcb sync` updates: * `[dependencies]`: direct dependencies the package imports or explicitly owns. * `[dependencies.indirect]`: the tool-managed MVS closure needed to build it. ```toml theme={null} [dependencies] "code.diode.computer/diode/registry/modules/Regulator" = "1.0" [dependencies.indirect] "code.diode.computer/diode/registry/components/TPS54331@1" = "1.0.2" "code.diode.computer/diode/registry/modules/Feedback@1" = "1.1.0" ``` The `@1` suffix is a compatibility lane. It allows multiple incompatible versions of the same package path to coexist while keeping the selected version exact. Do not edit `[dependencies.indirect]` by hand. Commit hydrated `pcb.toml` files. ## Vendoring (`[workspace].vendor`) Vendoring policy is controlled by the root workspace manifest: ```toml theme={null} [workspace] vendor = ["github.com/myorg/**"] ``` * `pcb publish` uses `[workspace].vendor` patterns when staging release sources. * `pcb sync` vendors packages matched by `[workspace].vendor`. * `pcb vendor` without `--all` uses `[workspace].vendor`. * `pcb vendor --all` vendors everything. * Read commands such as `pcb build`, `pcb layout`, `pcb test`, `pcb open`, and `pcb bom` do not change `vendor/` or rewrite dependency manifests. ## Workspace name (`[workspace].name`) Workspace manifests can override the Diode workspace name used for board release uploads: ```toml theme={null} [workspace] name = "my-workspace" ``` If `name` is omitted, `pcb publish` derives the workspace name from the first path segment of `[workspace].repository`. For example, `anything.com/XYZ/boards/MyBoard` uses `XYZ`. ## Endpoint (`[workspace].endpoint`) Workspace manifests can override the Diode host suffix used by CLI commands that access Diode services: ```toml theme={null} [workspace] endpoint = "diode.computer" ``` * `endpoint = "diode.computer"` resolves application and API URLs under `app.diode.computer` and `api.diode.computer`. * The setting applies to workspace-aware commands such as `pcb auth`, `pcb bom`, `pcb publish`, `pcb preview`, and routing commands. * Authentication is scoped to the resolved endpoint. Authentication for one endpoint does not overwrite tokens for another. ## BOM matching (`[workspace.bom]`) `pcb bom` availability queries use strict BOM matching by default, requiring exact MPN matches. Workspace manifests can opt out to use fuzzy matching: ```toml theme={null} [workspace.bom] strict = false ``` ## Registry search scope Registry-backed `pcb search` searches the public Diode registry and the registries configured by `[workspace].repository`. * `pcb search --registry code.diode.computer/diode/registry ...` overrides the default scope for that invocation. * Repeat `--registry` to search more than one registry. ## Pseudo-versions Pseudo-versions identify unreleased commits while preserving version ordering. The format is `v-0.-`. ```toml theme={null} [dependencies] # Branch reference - resolved to pseudo-version "github.com/acme/component-lib" = { branch = "main" } # Specific commit "github.com/acme/component-lib" = { rev = "a1b2c3d4" } ``` Resolution produces a version such as: ``` v0.3.15-0.20251120004415-137e2dcabc28… # commit hash shortened here for readability ``` `pcb sync` writes the resolved pseudo-version back to the package manifest with the full 40-character commit hash. The base version (0.3.15) is the next patch version after the most recent tag reachable from that commit. This places the pseudo-version after its base tag and before the next release. If the package has never been tagged, pseudo-versions start in the `0.1.1` family (for example `0.1.1-0.-`), one patch above the initial unpublished release version `0.1.0`. Pseudo-versions participate fully in MVS. If one package requires `component-lib@0.3.14` and another requires the pseudo-version above, MVS selects the pseudo-version (it is higher). This permits testing an unreleased change without retaining a mutable branch reference in the hydrated graph. Use a tagged release for production dependencies when one is available. ## Commands ### `pcb migrate` Runs project migrations using the latest stable `pcbc` toolchain, regardless of the workspace's current `pcb-version` lane. After all migrations succeed, the command updates `[workspace].pcb-version` in `pcb.toml` to the target toolchain lane. ```bash theme={null} pcb migrate pcb migrate ./path/to/workspace ``` ### `pcb sync` Reconciles imports and hydrates package manifests. Run this after adding or removing imports or changing dependency versions. ```bash theme={null} pcb sync # Sync packages under the current workspace/package pcb sync --check # CI guard: fail if pcb.toml or vendor/ is out of sync pcb sync -v # Print changed manifests ``` The command also downloads selected packages into the cache and vendors packages matched by `[workspace].vendor`. `pcb sync --check` always verifies the whole workspace, regardless of the current directory, and writes neither `pcb.toml` nor `vendor/`. It detects missing or stale vendored package versions; it does not verify the contents of vendored versions that are already present. ### `pcb add` Adds or upgrades a direct dependency for the package in the current directory. ```bash theme={null} pcb add github.com/acme/regulators/Buck@1.2.3 pcb add github.com/acme/regulators/Buck@latest pcb add -u # Upgrade all direct remote dependencies pcb add -u github.com/acme/regulators/Buck ``` `pcb add` rewrites the direct dependency entry and rehydrates the package's dependency closure. ### `pcb build` Builds a board or workspace package. ```bash theme={null} pcb build # Build default board pcb build WV0002.zen # Build a specific board file pcb build --offline # Build using only cached/vendored packages ``` `pcb build` checks that the hydrated state is sufficient and does not rewrite `pcb.toml` or `vendor/`. Use `pcb sync` or `pcb vendor` to update dependency state. ### `pcb list` Lists read-only package dependency information. ```bash theme={null} pcb list -m -u # Show compatible updates for direct dependencies pcb list -m -versions github.com/acme/foo # Show published versions for a dependency ``` `pcb list -m -u` must be run from a package directory. It reports direct remote dependencies only, showing the latest stable version in the same compatibility lane and the latest newer breaking lane when available. It does not update manifests. ### `pcb update` `pcb update` is disabled. Use `pcb add -u` instead. ```bash theme={null} pcb add -u # Upgrade all direct remote dependencies pcb add -u github.com/acme/regulators/Buck ``` ### `pcb publish` Publishes packages by creating annotated git tags. Discovers which packages have changed since their last published version and tags them. ```bash theme={null} pcb publish # Publish all changed packages pcb publish --bump=infer # Infer bumps from commit history and dependency waves pcb publish --bump=infer -y # Skip the final publish confirmation pcb publish --force # Skip preflight checks ``` A package requires publication when: * No version tag exists. * Its content hash differs from the published tag. * Its `pcb.toml` hash differs from the published tag. Versions are computed automatically: * **Unpublished:** Start at `0.1.0`. * **Published packages:** Apply the requested semantic-version bump: `patch`, `minor`, or `major`. * **`--bump=infer`:** Infer each bump from conventional commits since the last tag, then raise dependent bumps to at least the highest bump among published internal dependencies. * **`-y` / `--yes`:** Skip the final confirmation prompt. Packages are published in dependency order. Packages with no changed dependencies are published first; their dependents follow after manifest updates. ### `pcb info` Displays workspace and package information. ```bash theme={null} pcb info # Show workspace summary pcb info --format json # Machine-readable output ``` # Getting Started Source: https://docs.pcb.new/pages/quickstart Get started with the `pcb` CLI Use this guide to install `pcb`, create a board repository, validate its Zener source, and generate a KiCad layout. ## Requirements * macOS or Linux. Windows support is experimental; use WSL2 for the most stable Windows environment. * Git, which `pcb new board` uses to initialize a repository. * KiCad 10.x for layout generation and editing. `pcb build` does not require KiCad. ## 1. Install `pcb` The `pcb` launcher downloads and runs the `pcbc` toolchain selected by each project. Run the installer for your platform: ```bash bash theme={null} curl -fsSL https://raw.githubusercontent.com/diodeinc/pcb/main/install.sh | bash ``` ```powershell powershell theme={null} powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/diodeinc/pcb/main/install.ps1 | iex" ``` The installer adds `pcb` to your user `PATH` when necessary. Restart the shell if the command is not available, then verify the installation: ```bash theme={null} pcb --version ``` Install Rust, then clone the [repository](https://github.com/diodeinc/pcb) and run the local installer: ```bash theme={null} git clone https://github.com/diodeinc/pcb cd pcb ./install.sh --local ``` ## 2. Create a board repository Replace the example repository URL with the project repository URL: ```bash theme={null} pcb new board Blinky github.com/your-org/blinky cd Blinky ``` `pcb new board` creates `pcb.toml`, `Blinky.zen`, and a Git repository. The generated design is empty but valid. ## 3. Build the board Validate the board source and generate its netlist: ```bash theme={null} pcb build Blinky.zen ``` A successful build exits without validation errors. ## 4. Generate the layout Generate the KiCad files without opening KiCad: ```bash theme={null} pcb layout --no-open Blinky.zen ``` This command writes the generated board under `layout/`. ## Troubleshooting * If the shell cannot find `pcb` after installation, restart the shell or source the environment file printed by the installer. * If `pcb layout` cannot find KiCad, install `kicad-cli` and `pcbnew`. Set `KICAD_CLI` or `KICAD_PCBNEW` if either executable is outside its default platform path. * Run `pcb help` or `pcb help ` for the complete CLI reference. # Registry Source: https://docs.pcb.new/pages/registry Browse and update reusable PCB packages in the Registry app # Registry app The [Registry app](https://registry.diode.computer) provides access to reusable components and circuit modules. Use it to inspect package contents, trace dependencies, or prepare a package change in a sandbox. ## Browse packages Open **Parts** in the sidebar, then select one of these views: * **Components** lists physical parts. A component page can include its symbol, footprint, 3D model, manufacturer part number, sourcing data, and package metadata. * **Modules** lists reusable circuits composed from components and other modules. A module page identifies its schematic entry point and package dependencies. Sort either table by the available column headers. Registry sidebar showing the Components and Modules views Component page showing a symbol, footprint, 3D model, and part metadata Module page showing a schematic module and its dependencies ## Search Use the search field to find a component by manufacturer part number or description. Use the chat button when the selection requires comparison or design constraints. ## Inspect a package Select a package to open its read-only viewer. The package sidebar reports two dependency relationships: * **Includes** lists packages required by the selected package. * **Used by** lists packages that require the selected package. Review **Used by** before changing a public component or module interface. ## Change a package A sandbox is an isolated branch of the component library in which an agent can edit and validate packages. 1. Select the plus button beside **Sandboxes**. 2. Create a sandbox for the target registry. 3. Describe the required component or module change to the agent. 4. Review the generated files and validation results before merging the branch. Requests can identify a specific part, such as `TPSM86638RCGR`, or state design constraints, such as a 24 V to 5 V buck converter rated for at least 2 A. # Zener language Source: https://docs.pcb.new/pages/spec Language reference for Zener PCB designs # Specification ## Overview Zener is a domain-specific language built on [Starlark](https://github.com/bazelbuild/starlark/blob/master/spec.md) for describing PCB schematics. It adds components, symbols, nets, interfaces, physical quantities, and hierarchical circuit modules. This specification defines the Zener extensions. The [Starlark specification](https://github.com/bazelbuild/starlark/blob/master/spec.md) and the [starlark-rust type extensions](https://github.com/facebook/starlark-rust/blob/main/docs/types.md) define the underlying language. ## Modules and imports Each `.zen` file is a Starlark module that can be used in two ways: 1. **Symbol imports** with `load()` bring functions and types into scope: ```python theme={null} load("./utils.zen", "helper") load("@stdlib/units.zen", "Voltage", "Resistance") ``` 2. **Schematic modules** with `Module()` create instantiable subcircuits: ```python theme={null} Resistor = Module("@stdlib/generics/Resistor.zen") Resistor(name="R1", value="10k", P1=vcc, P2=gnd) ``` ### Import paths Import paths support local files, stdlib, and remote packages: ```python theme={null} # Local file (relative to current file) load("./utils.zen", "helper") # Stdlib (version controlled by toolchain) load("@stdlib/units.zen", "Voltage", "Resistance") load("@stdlib/interfaces.zen", "Spi") # Remote packages (version declared in pcb.toml) Resistor = Module("@stdlib/generics/Resistor.zen") TPS54331 = Module("code.diode.computer/diode/registry/components/TPS54331/TPS54331.zen") ``` The toolchain supplies the virtual `@stdlib` package. Do not declare it in `[dependencies]`. Remote package URLs omit versions. Declare each version in `pcb.toml` so import statements remain stable across upgrades: ```toml theme={null} [dependencies] "code.diode.computer/diode/registry/components/TPS54331" = "1.0" ``` ### Dependency resolution Remote package URLs do not include versions. Declare versions in `pcb.toml`, then run `pcb sync` to reconcile imports and hydrate dependency manifests. `@stdlib` is toolchain-managed and implicit. See [Packages](/pages/packages) for version resolution, package manifests, and dependency commands. ### Repository layouts Zener projects use one of two repository shapes: a board repository or a registry repository. #### Board repository A board repository contains one board plus any local modules and components it owns. Create one with `pcb new board `: ``` MainBoard/ ├── pcb.toml # Workspace and board manifest ├── MainBoard.zen ├── modules/ │ └── PowerSupply/ │ ├── pcb.toml # Package manifest │ └── PowerSupply.zen └── components/ └── TPS54331/ ├── pcb.toml └── TPS54331.zen ``` Use `pcb new package ` to add modules or components inside the board repository. **Manifest** (root `pcb.toml`): ```toml theme={null} [workspace] repository = "github.com/myorg/MainBoard" pcb-version = "0.4" endpoint = "diode.computer" [board] name = "MainBoard" path = "MainBoard.zen" description = "Replace with concise board description." ``` * `name`: Optional Diode workspace name override for board release uploads. When omitted, publish uses the first path segment of `repository`, such as `myorg` from `github.com/myorg/MainBoard`. * `repository`: Git remote URL (used to derive package URLs for publishing) * `pcb-version`: Required `pcbc` toolchain lane, such as `"0.3"`, `"0.4"`, or `"nightly"`. `pcb` selects the newest patch in a semver lane, installing it if needed. Project commands use this lane. `pcb auth` and `pcb migrate` use latest stable; `pcb migrate` updates this field after successful migrations. An explicit `+` override always wins. * `endpoint`: Optional Diode host suffix used for workspace-scoped app/API URLs. `"diode.computer"` resolves to `app.diode.computer` and `api.diode.computer`. * `[board]`: Defines the repository's buildable board with `name`, entry `path`, and a short `description` Older workspace formats are no longer supported. Update legacy manifests and imports before building with the current toolchain. Only the workspace root `pcb.toml` may contain a `[workspace]` section. Board dependencies are also declared in the root manifest's `[dependencies]` table. #### Registry repository A registry repository contains reusable packages but no board at the root. It uses top-level `components/` and `modules/` directories: ``` registry/ ├── pcb.toml # Workspace manifest ├── components/ │ └── TPS54331/ │ ├── pcb.toml # Package manifest │ ├── TPS54331.zen │ ├── TPS54331.kicad_sym │ └── TPS54331.kicad_mod └── modules/ └── UsbCSink/ ├── pcb.toml # Package manifest └── UsbCSink.zen ``` Registry manifests include `[workspace]` but do not include `[board]`: ```toml theme={null} [workspace] repository = "github.com/myorg/registry" pcb-version = "0.4" ``` **Package manifest** (e.g., `modules/PowerSupply/pcb.toml`): ```toml theme={null} [dependencies] "code.diode.computer/diode/registry/components/TPS54331" = "1.0" parts = [ { mpn = "TPS54331DR", symbol = "TPS54331.kicad_sym", symbol_name = "TPS54331", manufacturer = "Texas Instruments", qualifications = ["Preferred"] }, ] ``` Module and component packages omit `[board]` because boards and other modules instantiate them as libraries. * `[dependencies]`: Version constraints for packages imported by this package * `parts`: Optional default sourcing metadata keyed by symbol file. Each entry provides `mpn`, `manufacturer`, optional `qualifications`, optional `datasheet`, a package-relative `.kicad_sym` `symbol` path, and optional `symbol_name` to target a specific symbol within a multi-symbol library file. If `symbol_name` is omitted, the referenced `.kicad_sym` file must contain exactly one symbol; otherwise resolution fails and `symbol_name` is required. `Component()` sourcing precedence is described below. See [Packages](/pages/packages) for the complete manifest reference. ### Prelude These stdlib symbols are available in every user `.zen` file without `load()`: * `io`, `input`, `output` — from `@stdlib/io.zen` * `Net`, `Power`, `Ground`, and the `NotConnected()` open-net constructor — from `@stdlib/interfaces.zen` * `Board` — from `@stdlib/board_config.zen` * `Layout`, `Part` — from `@stdlib/properties.zen` Local definitions shadow prelude symbols. The prelude does not apply to stdlib modules themselves. ## Nets and interfaces ### Nets A `Net` represents an electrical connection between component pins. `Net` is the base net type; specialized types like `Power` and `Ground` add metadata (schematic symbols, voltage) while remaining fundamentally nets. `NotConnected` is a constructor for an intentionally open net, not a net type. ```python theme={null} load("@stdlib/units.zen", "Impedance") # Basic nets CLK = Net() DATA = Net(impedance=Impedance(50)) # controlled impedance VREF = Net() # inferred from assignment # Power and ground (prelude — no load needed) VCC = Power("VCC_3V3", voltage="3.3V") GND = Ground() # inferred from assignment; voltage defaults to 0V # Intentionally open nc = NotConnected() ``` `Net(name_or_net=None, voltage=None, impedance=None)` accepts a positional-only name string or existing net to cast. `Power` and `Ground` also accept `voltage`. Additional connected net types (`Analog`, `Pwm`, `Gpio`) are available from `@stdlib/interfaces.zen`. `NotConnected` is reserved for the open-net constructor; `builtin.net_type("NotConnected")` is invalid. If a net constructor omits `name`, the assigned variable name is used when available: ```python theme={null} CLK = Net() # equivalent to Net("CLK") VDD = Power() # equivalent to Power("VDD") ``` If the explicit name duplicates the inferred assignment name, Zener reports style advice because the explicit name is redundant. Explicit constructor names still win. Regular nets must either have a name or be assigned where a name can be inferred: ```python theme={null} alias = Net("CLK") # name is "CLK", not "alias" Net() # error: Net is unnamed ``` Regular net names must be unique. Duplicate regular net names are rejected; open `NotConnected()` nets are exempt. `NotConnected()` has no source-level name. A supplied name is ignored with a warning. Downstream tools assign connection-derived names where required. Typed net fields validate against their declared field type and use the same string coercions as module inputs, so `Power("VCC", voltage="3.3V")` is equivalent to `Power("VCC", voltage=Voltage("3.3V"))`. Net type annotations on `io()` boundaries check electrical compatibility; they do not change whether a provided value is connected or intentionally open. `NotConnected()` can satisfy any net-shaped `io()` because it is an open net value, and it remains `NotConnected` in the emitted netlist. Specialized connected types can be viewed as `Net`; plain connected `Net` values do not automatically promote to specialized types. Explicit constructors assert the resulting connected net type. For example, `Net(NotConnected())` constructs a regular connected `Net`, while passing `NotConnected()` directly to `io(Net)` keeps it open. `NotConnected` itself is not valid where a type is expected, such as `io(NotConnected)`. ### Interfaces Interfaces define reusable connection patterns — groups of related nets. Define custom interfaces with `interface()`: ```python theme={null} MyBus = interface( clk = Net("CLK"), data = Net("DATA"), enable = field(bool, True), ) bus = MyBus("BUS1", enable=False) debug_bus = MyBus() ``` Interface fields can be net instances, interface instances (for hierarchical composition), or `field()` specs. When instantiated, the first positional argument is an optional name; named arguments override defaults. If an interface instance omits its explicit name, the assigned variable name becomes the root used for generated child nets: ```python theme={null} PowerIf = interface( vcc = Net(), gnd = Net("GND"), ) power = PowerIf() # generated child nets become power_vcc and power_GND ``` Only values generated by the interface definition are renamed this way. Caller-provided nets or interface instances keep their existing names: ```python theme={null} ext = Net("EXT") power = PowerIf(vcc = ext) # ext stays "EXT" ``` The standard library provides common interfaces (`Spi`, `I2c`, `Uart`, `Usb2`, `DiffPair`, `Pcie`, `Jtag`, `Swd`, etc.) in `@stdlib/interfaces.zen`. Helper functions `UartPair(a, b)` and `UsartPair(a, b)` create cross-connected pairs for point-to-point links. ## Components and symbols ### Component Components represent physical electronic parts with pins, a schematic symbol, and a PCB footprint. ```python theme={null} Component( name = "U1", symbol = my_symbol, pins = { "VCC": vcc, "GND": gnd, "OUT": output_net, }, prefix = "U", part = Part(mpn="LM358", manufacturer="TI"), ) ``` **Constructor**: `Component(**kwargs)` | Parameter | Required | Description | | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | yes | Instance name | | `symbol` | yes | Symbol object defining the schematic representation | | `pins` | yes | Dict mapping pin names to nets; omit KiCad `no_connect` pins | | `part` | no | `Part` object specifying manufacturer sourcing (preferred) | | `prefix` | no | Reference designator prefix (default: `"U"`) | | `manufacturer` | no | Manufacturer name (legacy — prefer `part`) | | `mpn` | no | Manufacturer part number (legacy — prefer `part`) | | `footprint` | no | PCB footprint path (default: inferred from symbol `Footprint` property) | | `type` | no | Component type string | | `properties` | no | Additional properties dict | | `spice_model` | no | Explicit `SpiceModel`; default: inferred from symbol `Sim.*` properties when present | | `dnp` | no | Do Not Populate flag | | `skip_bom` | no | Exclude from BOM (default: inverse of symbol `in_bom` flag) | | `datasheet` | no | Datasheet URL or path (default: `part.datasheet`, then this component value, then symbol `Datasheet` property; local component paths resolved relative to the `.zen` file, symbol-local paths resolved relative to the `.kicad_sym` file) | When KiCad symbol pin metadata is available: * omitted `no_connect` pins are auto-wired to `NotConnected()` * explicit `no_connect` entries warn * `power_in` and `power_out` pins warn if connected to plain `Net` instead of `Power` or `Ground` * if `spice_model` is omitted and the symbol provides `Sim.Library`, `Sim.Name`, `Sim.Device=SUBCKT`, `Sim.Pins`, and optional `Sim.Params`, `Component()` derives the SPICE model from those symbol properties ### Part `Part` specifies manufacturer sourcing for a component. It is a prelude symbol — available in all `.zen` files without `load()`. **Constructor**: `Part(mpn, manufacturer, qualifications=[], datasheet=None)` | Parameter | Required | Description | | ---------------- | -------- | --------------------------------------------------- | | `mpn` | yes | Manufacturer part number (non-empty string) | | `manufacturer` | yes | Manufacturer name (non-empty string) | | `qualifications` | no | List of qualification strings (e.g. `["AEC-Q200"]`) | | `datasheet` | no | Datasheet URL or path for this manufacturer part | **Attributes**: `.mpn`, `.manufacturer`, `.qualifications`, `.datasheet` Use `Part` with the `part` parameter on `Component()` for primary sourcing, and in `properties["alternatives"]` for alternate parts: ```python theme={null} Component( name = "R1", symbol = Symbol(library="Device.kicad_sym", name="R"), pins = {"P1": vcc, "P2": gnd}, part = Part( mpn = "RC0603FR-0710KL", manufacturer = "Yageo", qualifications = ["AEC-Q200"], datasheet = "https://www.yageo.com/upload/media/product/products/datasheet/rchip/RC_L_51_RoHS_L_6.pdf", ), properties = { "alternatives": [ Part(mpn="ERJ-3EKF1001V", manufacturer="Panasonic"), ], }, ) ``` During `pcb build`, reference designators are automatically allocated per-prefix (e.g. `R1`, `R2`, `C1`). ### Symbol A `Symbol` represents a schematic symbol loaded from a KiCad symbol library. ```python theme={null} ic_symbol = Symbol(library="TCA9554DBR.kicad_sym") connector = Symbol(library="Connector_Generic.kicad_sym", name="Conn_01x14") ``` **Constructor**: `Symbol(library, name=None)` * `library`: Path to a `.kicad_sym` file. Supports local paths and package paths. * `name`: Symbol name within the library. Required for multi-symbol libraries; omit for single-symbol files. ### Physical quantities Physical quantities have a nominal value, optional min/max bounds, and dimensional units. Zener exposes five SI bases as `builtin.Mass`, `builtin.Length`, `builtin.Time`, `builtin.Current`, and `builtin.Temperature`. `@stdlib/units.zen` re-exports them and derives conventional electrical types such as `Voltage`, `Resistance`, `Capacitance`, `Inductance`, `Impedance`, and `Frequency` using multiplication and division. For example, `Voltage` is `Mass * Length * Length / (Current * Time * Time * Time)`. ```python theme={null} load("@stdlib/units.zen", "Voltage", "Current", "Time", "Resistance", "Capacitance") # Point values supply = Voltage("3.3V") resistor = Resistance("4k7") # 4.7kΩ using resistor notation cap = Capacitance("100nF") # Ranges input_range = Voltage("1.1–3.6V") explicit_nominal = Voltage("11–26V (12V)") # Keyword bounds operating = Voltage(min=11, max=26) # Arithmetic with automatic unit tracking power = Voltage("3.3V") * Current("0.5A") # → 1.65W r = Voltage("5V") / Current("100mA") # → 50Ω # The same algebra constructs declarable physical types SlewRate = Voltage / Time slew_rate = config(SlewRate, default="5V/us") ``` Derived types are dimensional rather than nominal: independently constructed types with the same dimensions accept the same values. For example, `Voltage / Current` and stdlib `Resistance` are the same physical type. SI base symbols are resolved in the context of the declared type, so `Length("1m")` is one metre. Untyped `"1m"` parsing retains its historical meaning of one milliohm for compatibility. **Properties**: `.value` (alias for `.nominal`), `.nominal`, `.min`, `.max`, `.tolerance`, `.unit` **Methods**: `.with_tolerance(t)`, `.with_value(v)`, `.with_unit(u)`, `.abs()`, `.diff(other)`, `.within(other)`, `.matches(other)`, `.spice()` **Operators**: `+`, `-`, `*`, `/` (with unit tracking), `<`, `>`, `<=`, `>=`, `==` (strict equality against another `PhysicalValue`), unary `-` Use `.matches(other)` for coercive comparisons against strings or scalars, such as `Voltage("5V").matches("5V")`. **String formatting**: Point values → `"3.3V"`. Symmetric tolerances → `"10k 5%"`. Ranges → `"11–26V (16V nom.)"`. **SPICE formatting**: Pass a `PhysicalValue` directly or use `.spice()` to emit an ngspice-safe string (`meg` for mega, no unit suffix, e.g. `Resistance("2MOhm")` → `2meg`). ### Generic components Prefer generic components over raw `Component()` where possible. Generics come with standard symbols, footprints, and automatic BOM matching to house parts. ```python theme={null} Resistor = Module("@stdlib/generics/Resistor.zen") Capacitor = Module("@stdlib/generics/Capacitor.zen") Resistor(name="R1", value="10k", package="0603", P1=vcc, P2=gnd) Capacitor(name="C1", value="100nF", package="0402", voltage="16V", P1=vcc, P2=gnd) ``` See `@stdlib/generics/` for the full list of available generics and their accepted parameters. ## Modules Modules are reusable subcircuits — `.zen` files that declare their electrical interface and configuration, then build a circuit from them. They are the primary mechanism for hierarchical design. ### Module() `Module()` loads a `.zen` file and returns a callable that instantiates it as a subcircuit: ```python theme={null} PowerSupply = Module("./modules/PowerSupply.zen") TPS54331 = Module("code.diode.computer/diode/registry/components/TPS54331/TPS54331.zen") ``` Instantiation takes a required `name` and passes remaining arguments as inputs to the module's `io()` and `config()` declarations: ```python theme={null} PowerSupply(name="PSU1", VIN=vin, VOUT=vout, GND=gnd, output_voltage="3.3V") ``` Additional instantiation parameters: * `properties`: Dict of property overrides for the module instance. * `dnp`: Bool — mark as Do Not Populate. * `schematic`: `"collapse"` or `"embed"` — controls schematic rendering of the subcircuit. ### io() Declare a net or interface input for a module. This defines the module's electrical interface — the nets that a parent must (or may) connect when instantiating it. `io`, `input`, and `output` are prelude symbols re-exported from `@stdlib/io.zen`. The low-level builtin is `builtin.io(...)`. **Signature:** `io(name, typ_or_template, checks=None, optional=False, help=None, direction=None)` or `io(typ_or_template, checks=None, optional=False, help=None, direction=None)` * `name`: Optional explicit input name (conventionally UPPERCASE). If omitted, `io()` must be assigned to a top-level variable and that variable name is used. * `typ_or_template`: A net type (`Net`, `Power`, `Ground`, etc.), an interface factory (`Spi`, `Uart`, etc.), a net template value, or an interface template value. * `checks`: Optional check function or list of checks applied to the resolved value. * `optional`: If `True`, use a generated net or interface when the parent omits the input. The default is `False`. * `help`: Help text for documentation and signatures. * `direction`: Optional signature metadata. Must be `"input"` or `"output"` when provided. ```python theme={null} VCC = io(Power) GND = io(Ground) VDD_3V3 = io(Power(voltage="3.3V")) SPI_BUS = io(Spi("SPI"), optional=True) SPI = io(Spi, optional=True) DATA = io(Net) CS = io(Net) VIN = io(Power, direction="input") VOUT = io(Power, direction="output") ``` When `typ_or_template` is a template value, `io()` derives all three of these from it: * the placeholder type * the default/template metadata * implicit checks that constrain any provided input For example, `io(Power(voltage="1.8V - 3.6V"))` requires any supplied `Power` net to stay within that voltage range. Use `direction` only for one-way signal or power flow. Leave shared rails such as `GND` as plain `io()` declarations. When the explicit `name` matches the assigned variable name, omitting it is the preferred style and emits no redundancy advice. `input(name, typ_or_template, ...)` and `output(name, typ_or_template, ...)` are equivalent to `io(...)` with `direction="input"` or `direction="output"` respectively, and they also support omitted explicit names when assigned to top-level variables. ```python theme={null} VIN = input(Power) VDD = input(Power(voltage="3.3V")) VOUT = output(Power) CS = input(Net) ``` ### config() Declare a typed configuration input for a module. This defines parameters that control the module's behavior — values (not nets) provided by the parent. **Signature:** `config(name, typ, checks=None, default=None, allowed=None, optional=None, help=None)` or `config(typ, checks=None, default=None, allowed=None, optional=None, help=None)` * `name`: Optional explicit input name (conventionally lowercase). If omitted, `config()` must be assigned to a top-level variable and that variable name is used. * `typ`: Expected type — primitives (`str`, `int`, `float`, `bool`), `enum`, or physical quantity constructors. `record()` types are not supported as module `config()` inputs. * `checks`: Optional check function or list of checks. * `default`: Default value. When provided, `optional` defaults to `True`. * `allowed`: Optional finite set of allowed values. Accepts a `list`, `tuple`, or `dict` (using only the keys). Supported for `str`, `int`, `float`, `bool`, `enum`, and physical quantity types. * `optional`: Explicit override. When `True` with no default, returns `None`. * `help`: Help text. ```python theme={null} value = config(Resistance) package = config(Package, default=Package("0603")) voltage = config(Voltage, optional=True) manufacturer = config(str, default="Acme") output_voltage = config( "output_voltage", Voltage, allowed=["0.8V", "0.9V", "1.0V", "1.1V"], default="1.0V", ) ``` Values passed by the parent are automatically converted to the declared type when possible. String inputs can coerce to primitives (`"true"` → `True`, `"42"` → `42`, `"3.3"` → `3.3`), physical quantities (`"10k"` → `Resistance("10k")`), and enum variants (`"0603"` → `Package("0603")`). This is why `Resistor(name="R1", value="10k", package="0603", ...)` works even though `value` expects `Resistance` and `package` expects `Package`. When `allowed` is present, both the allowed set and the provided value are normalized through that same coercion path before membership is checked, and physical values are surfaced using their canonical formatting. As with `io()`, repeating the assigned variable name as an explicit `config()` name is redundant and triggers a style advice. ### Write a module A module is a `.zen` file that declares its interface with `io()` and `config()`, then uses those values to build its circuit: ```python theme={null} # modules/LedIndicator.zen Resistor = Module("@stdlib/generics/Resistor.zen") Led = Module("@stdlib/generics/Led.zen") # Configuration color = config(str, default="red") r_value = config(str, default="330ohms") # Electrical interface VCC = io(Power) GND = io(Ground) # Internal net led_anode = Net("LED_ANODE") # Circuit Resistor(name="R1", value=r_value, package="0603", P1=VCC, P2=led_anode) Led(name="D1", color=color, package="0603", A=led_anode, K=GND) # Layout Layout(name="LedIndicator", path="layout/LedIndicator") ``` Instantiated by a parent board: ```python theme={null} # MainBoard.zen LedIndicator = Module("./modules/LedIndicator.zen") vcc = Power("VCC_3V3", voltage="3.3V") gnd = Ground("GND") LedIndicator(name="LED1", VCC=vcc, GND=gnd, color="green") LedIndicator(name="LED2", VCC=vcc, GND=gnd, color="red") Board(name="MainBoard", layers=4, layout_path="layout/MainBoard") ``` ## Utilities ### Board and layout `Board()` configures PCB manufacturing parameters — stackup, design rules, and layout path. It is a prelude symbol. ```python theme={null} Board(name="my_board", layers=4, layout_path="layout/my_board") ``` Key parameters: `name`, `layout_path`, `layers` (2/4/6/8/10), `config` (explicit `BoardConfig`), `outer_copper_weight` (`"1oz"` or `"2oz"`), `copper_finish` (default `"ENIG"`). When `layers` is provided, `Board()` selects an appropriate default stackup, netclasses, and design rules. An explicit `config` is merged on top. See `@stdlib/board_config.zen` for `BoardConfig`, `Stackup`, `DesignRules`, `NetClass`, and preset stackups. `Layout()` defines reusable layout blocks for modules. When writing a module, use `Layout(name, path)` to associate a PCB layout with the subcircuit. See `@stdlib/properties.zen`. **`Simulation(name, setup=None, modifiers=None, bom_profile=...)`** — Attach inline simulation setup and component modifiers to the current module. `Simulation()` uses the same BOM-profile hook as `Layout()`: by default it registers the standard house-part matcher, `modifiers` run before `bom_profile`, and `bom_profile=None` disables automatic house matching for simulation-only evals. ### File and path **`File(path)`** — Resolve an existing path relative to the current `.zen` file. The call fails if the path does not exist. ```python theme={null} datasheet = File("TPS54331.pdf") footprint = File("Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod") ``` **`Path(path, allow_not_exist=False)`** — Like `File()` but supports package paths and optional non-existence. ```python theme={null} layout_dir = Path("layout/my_board", allow_not_exist=True) ``` ### Assertions Three global functions for validation and diagnostics: * **`check(condition, message)`** — Assert a condition. Raises an error with `message` if `condition` is false. * **`error(message)`** — Raise an error unconditionally. * **`warn(message)`** — Emit a warning diagnostic. ```python theme={null} check(voltage <= Voltage("3.6V"), "Voltage exceeds maximum rating") warn("Using deprecated parameter") ``` ### Electrical checks `@stdlib/checks.zen` provides reusable check functions for typed inputs. For example, `voltage_within(range)` validates that a net with voltage metadata, or a direct `Voltage` value, falls within a specified range: ```python theme={null} load("@stdlib/checks.zen", "voltage_within") vref = config(Voltage, checks=voltage_within("1.1–3.6V")) ``` Template-first `io()` can also contribute implicit checks. A typed net template with a meaningful `voltage` property enforces the same containment rule automatically: ```python theme={null} VCC = io(Power(voltage="1.1–3.6V")) ``` Explicit `checks=` still run as well, after type validation and any template-derived implicit checks. ### E-series `@stdlib/utils.zen` provides functions to snap values to standard resistor/capacitor E-series: `e3()`, `e6()`, `e12()`, `e24()`, `e48()`, `e96()`, `e192()`. ```python theme={null} load("@stdlib/utils.zen", "e96") r = e96(Resistance("4.8k")) # → 4.87kΩ (nearest E96 value) ``` ## Schematic position comments Zener supports persisted schematic placement metadata in trailing comment blocks. These comments are consumed by tooling and surfaced in netlist output. **Do not edit these comments directly.** Canonical line format: ```text theme={null} # pcb:sch x= y= rot= [mirror=] ``` * `id`: Position key (component or net symbol key in comment form, e.g. `R1`, `VCC.1`) * `x`, `y`: Schematic coordinates * `rot`: Rotation in degrees * `mirror` (optional): Mirror axis (`x` or `y`) Examples: ```text theme={null} # pcb:sch R1 x=100.0000 y=200.0000 rot=0 # pcb:sch U1 x=150.0000 y=200.0000 rot=90 mirror=x ``` # Testing Source: https://docs.pcb.new/pages/testing Validate Zener modules with test benches and circuit graphs # Testing Zener tests validate module connectivity, component properties, and circuit topology. Define tests in `.zen` files and run them with `pcb test`. ## Define a test bench `TestBench` evaluates a module for one or more input cases and runs each check function against the result: ```python theme={null} MyCircuit = Module("./my_circuit.zen") def verify_power_connections(module, inputs): connections = module.nets.get("VCC", []) check(len(connections) >= 2, "VCC must have at least two connections") def verify_ground(module, inputs): check("GND" in module.nets, "GND net is missing") TestBench( name = "PowerTest", module = MyCircuit, test_cases = { "default": {}, }, checks = [verify_power_connections, verify_ground], ) ``` This example assumes that `MyCircuit` has no required inputs. Supply each module input in the case dictionary when the module requires arguments. | Parameter | Description | | ------------ | ---------------------------------------------------------- | | `name` | Test bench identifier. | | `module` | Module loader created with `Module()`. | | `test_cases` | Nonempty map from case names to module input dictionaries. | | `checks` | Functions to run for each evaluated case. | Each check receives the evaluated module and the active case's input dictionary. Call `check(condition, message)` or `error(message)` to fail a test. An unhandled evaluation error also fails the test. `pcb test` reports failures with their source locations. ## Inspect the evaluated module The evaluated module exposes its nets and components: | Expression | Result | | -------------------- | ------------------------------------------------------ | | `module.nets` | Map from net names to connected component-port tuples. | | `module.components` | Map from hierarchical paths to components. | | `module["U1"]` | Direct child component or module. | | `module["Power.U1"]` | Descendant selected by hierarchical path. | Component values expose `name`, `type`, `pins`, `properties`, sourcing fields, and component-specific properties such as `resistance` when present. ```python theme={null} def verify_feedback_resistor(module, inputs): resistor = module["Feedback.R1"] check(resistor.type == "resistor", "Feedback.R1 must be a resistor") check(resistor.resistance.matches("10k"), "Feedback.R1 must be 10 kohm") ``` ## Search circuit paths `module.graph()` returns the circuit graph. Use `graph.paths()` to find simple paths between component pins or public module nets: ```python theme={null} def verify_power_path(module, inputs): graph = module.graph() paths = graph.paths( start = ("Regulator", "VIN"), end = "GND_GND", max_depth = 5, ) check(len(paths) > 0, "No path exists from VIN to ground") ``` `start` and `end` accept a `(component, pin)` tuple or the name of a public net. `max_depth` limits the number of traversed components and defaults to 10. Each returned path provides: * `ports`: traversed `(component, pin)` tuples * `components`: traversed component values * `nets`: traversed net names ## Match components in a path `count`, `any`, `all`, and `none` accept a function that validates one component. The matcher succeeds when it returns without an error: ```python theme={null} def is_resistor(component): check(component.type == "resistor", "component is not a resistor") resistor_count = path.count(is_resistor) path.any(is_resistor) path.all(is_resistor) path.none(is_resistor) ``` `any`, `all`, and `none` fail when their condition is not satisfied. ## Match a component sequence `path.matches()` validates the complete ordered component sequence. Each matcher receives the path and the current component index, then returns the number of components it consumed. ```python theme={null} def resistor(expected_value=None): def matcher(path, cursor): check(cursor < len(path.components), "expected a resistor at end of path") component = path.components[cursor] check(component.type == "resistor", component.name + " is not a resistor") if expected_value != None: check(component.resistance.matches(expected_value), "unexpected resistance") return 1 return matcher def capacitor(expected_value=None): def matcher(path, cursor): check(cursor < len(path.components), "expected a capacitor at end of path") component = path.components[cursor] check(component.type == "capacitor", component.name + " is not a capacitor") if expected_value != None: check(component.capacitance.matches(expected_value), "unexpected capacitance") return 1 return matcher ``` Use the matchers in a topology check: ```python theme={null} def verify_filter(module, inputs): paths = module.graph().paths(start=("OpAmp", "OUT"), end="GND_GND") check(len(paths) > 0, "filter path is missing") paths[0].matches( resistor("1k"), capacitor("100nF"), resistor("10k"), ) ``` Matcher helpers are not prelude symbols. Define them in the test file or load them from a project-local helper module. Pass `suppress_errors=True` when a failed sequence is an expected search result. The method then returns `False` instead of failing the test: ```python theme={null} matching_paths = [path for path in paths if path.matches( resistor(), capacitor(), suppress_errors = True, )] check(len(matching_paths) > 0, "RC path is missing") ``` # Visual Studio Code extension Source: https://docs.pcb.new/pages/vscode Edit Zener files and inspect generated schematics in Visual Studio Code # Visual Studio Code extension The Zener extension adds language support and schematic previews for `.zen` files. It provides syntax highlighting, completion, hover information, diagnostics, definition navigation, and debugger integration. ## Install and open a schematic 1. Install the extension from the [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=diode-inc.zener) or [Open VSX](https://open-vsx.org/extension/diode-inc/zener). 2. Open a PCB workspace in Visual Studio Code. 3. Open a `.zen` file. 4. Select **Open Schematic** in the editor toolbar. The preview updates when the source changes. Diagnostics identify syntax, evaluation, and connection errors in the editor. Zener schematic preview in the light Visual Studio Code theme Zener schematic preview in the dark Visual Studio Code theme