Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.pcb.new/llms.txt

Use this file to discover all available pages before exploring further.

A compact antenna front end is a great Zener example because it uses all three layers of the language:
  • thin Component() wrappers for custom physical parts
  • a reusable Module() for the matching network
  • a top-level Board() that composes the RF path
This guide follows the workspace and package model from the language spec. In particular:
  • the top-level design uses Board(...), not Layout(...)
  • reusable blocks live in packages under modules/ and components/
  • local imports use normal paths

Workspace Layout

Start with a normal workspace and give each reusable block its own package:
my-rf-demo/
├── pcb.toml
├── boards/
│   └── AntennaDemo/
│       ├── AntennaDemo.zen
│       └── pcb.toml
├── modules/
│   └── AntennaMatch/
│       ├── AntennaMatch.zen
│       └── pcb.toml
└── components/
    ├── ChipAntenna/
    │   ├── ChipAntenna.zen
    │   └── pcb.toml
    └── SMAConnector/
        ├── SMAConnector.zen
        └── pcb.toml
The root pcb.toml can be as small as:
[workspace]
pcb-version = "0.3"
members = ["boards/*", "modules/*", "components/**"]
Use pcb new workspace, pcb new board, and pcb new package to create this structure. The values in this guide are starting points only. Real matching values should come from your antenna datasheet, PCB stackup, and VNA measurements.

1. Wrap The Physical Endpoints

For common passives, prefer the stdlib generics. For parts like an SMA connector or a specific chip antenna, a tiny custom package is often the right tool. Each .zen file is still a module. A custom “component package” is simply a small module that calls Component() once and exposes its pins through io().

SMA Connector

# 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 = "@kicad-symbols/Connector.kicad_sym",
        name = "Conn_Coaxial_Small",
    ),
    footprint = File(
        "@kicad-footprints/Connector_Coaxial.pretty/SMA_Amphenol_132134_Vertical.kicad_mod"
    ),
    prefix = prefix,
    pins = {
        "In": CENTER,
        "Ext": SHIELD,
    },
)

Chip Antenna

# components/ChipAntenna/ChipAntenna.zen
FEED = io(Net)

prefix = config(str, default="AE")

Component(
    name = "ChipAntenna",
    symbol = Symbol(
        library = "@kicad-symbols/Device.kicad_sym",
        name = "Antenna",
    ),
    footprint = File(
        "@kicad-footprints/RF_Antenna.pretty/Texas_SWRA416_868MHz_915MHz.kicad_mod"
    ),
    prefix = prefix,
    pins = {
        "A": FEED,
    },
)
Use raw Component() only for the parts that actually need it. The matching elements themselves should stay on the stdlib generics so you inherit current symbols, footprints, and BOM behavior automatically.
If you are authoring these files in the editor, hovering the pins field in a Component() call will show the pin names Zener expects from the symbol.

2. Build A Reusable Matching Module

This matching block reserves three tuning footprints: a series inductor, a shunt capacitor, and a final series resistor that can be used as a real damping element or simply stuffed as 0ohm.
# 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 the nets the parent board must connect, and plain Net() for internal nodes like MATCH_NODE that should stay inside the module boundary.
The parent can still pass strings like "6.8nH", "1.5pF", and "0ohm". Zener will convert them to physical quantities because the config() types are declared explicitly.

3. Compose The Board

Now instantiate the connector, matching network, and antenna on a real board:
# boards/AntennaDemo/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",
)
At this point the RF path is easy to read:
SMA center -> series inductor -> tuning node -> series resistor -> antenna feed
                                 |
                            shunt capacitor
                                 |
                               ground

Build And Iterate

Build and lay out the board with the normal CLI flow:
pcb build boards/AntennaDemo/AntennaDemo.zen
pcb layout boards/AntennaDemo/AntennaDemo.zen
If the design validates, KiCad will open with a generated board you can tune and refine.
Electrical correctness here depends heavily on placement. In practice, keep the matching parts as close to the antenna feed as possible, route the RF trace with the impedance your stackup expects, and leave room for value swaps during bringup.

Bonus: Add Mounting Holes

You can also add mounting holes programmatically:
MountingHole = Module("@stdlib/generics/MountingHole.zen")

for i in range(4):
    MountingHole(
        name = "H" + str(i + 1),
        diameter = "M2",
    )
Run pcb layout again and place the generated holes where they make sense for your enclosure and keep-out requirements.

Why This Pattern Scales

  • The antenna and connector packages stay small and easy to reuse.
  • The matching network owns the topology and tuning values.
  • The board file stays focused on composition instead of implementation detail.
That separation is the main benefit of Zener for RF work: you can change matching values, footprints, or even the whole antenna package without rewriting the board-level connectivity.