Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
382 changes: 382 additions & 0 deletions board_config.zen
Original file line number Diff line number Diff line change
@@ -0,0 +1,382 @@
# Copper constraints - all clearances and widths for copper features
Copper = record(
minimum_clearance=field(float | None, None), # Minimum clearance between copper features
minimum_track_width=field(float | None, None), # Minimum track width
minimum_connection_width=field(float | None, None), # Minimum connection width (KiCad: m_MinConn)
minimum_annular_width=field(float | None, None), # Minimum annular ring width for vias
minimum_via_diameter=field(float | None, None), # Minimum via diameter
copper_to_hole_clearance=field(float | None, None), # Clearance from copper to holes
copper_to_edge_clearance=field(float | None, None), # Clearance from copper to board edge
)

# Hole constraints - drill sizes and clearances
Holes = record(
minimum_through_hole=field(float | None, None), # Minimum through hole diameter
hole_to_hole_clearance=field(float | None, None), # Minimum clearance between holes
)

# Micro via constraints - specialized via rules
Uvias = record(
minimum_uvia_diameter=field(float | None, None), # Minimum micro via diameter
minimum_uvia_hole=field(float | None, None), # Minimum micro via hole diameter
)

# Silkscreen constraints - text and graphics rules
Silkscreen = record(
minimum_item_clearance=field(float | None, None), # Minimum clearance for silkscreen items
minimum_text_height=field(float | None, None), # Minimum text height
# Note: minimum_text_thickness is complex in KiCad API, not supported yet
)

# All design rule constraints grouped together
Constraints = record(
copper=field(Copper | None, None),
holes=field(Holes | None, None),
uvias=field(Uvias | None, None),
silkscreen=field(Silkscreen | None, None),
)

# Via dimensions for pre-defined sizes
ViaDimension = record(
diameter=field(float | None, None), # Via diameter in mm
drill=field(float | None, None), # Via drill size in mm
)

# NOTE: Differential pair dimensions are not supported due to missing SWIG template
# in KiCad Python API. The following would be needed in board.i:
# %template(DIFF_PAIR_DIMENSION_Vector) std::vector<DIFF_PAIR_DIMENSION>;
#
# DiffPairDimension = record(
# width=float, # Track width in mm
# gap=float, # Gap between tracks in mm
# via_gap=float, # Gap for vias in mm
# )

# Pre-defined sizes that appear in KiCad GUI dropdowns
PredefinedSizes = record(
track_widths=field(list | None, None), # List of track widths in mm (e.g. [0.1, 0.15, 0.2])
via_dimensions=field(list | None, None), # List of ViaDimension objects
# diff_pair_dimensions=list, # Not supported - missing KiCad SWIG template
)

# Netclass definition for PCB routing rules
NetClass = record(
name=field(str | None, None), # Netclass name
clearance=field(float | None, None), # Clearance in mm
track_width=field(float | None, None), # Track width in mm
via_diameter=field(float | None, None), # Via diameter in mm
via_drill=field(float | None, None), # Via drill/hole in mm
microvia_diameter=field(float | None, None), # Microvia diameter in mm
microvia_drill=field(float | None, None), # Microvia drill/hole in mm
diff_pair_width=field(float | None, None), # Differential pair width in mm
diff_pair_gap=field(float | None, None), # Differential pair gap in mm
diff_pair_via_gap=field(float | None, None), # Differential pair via gap in mm
priority=field(int | None, None), # Priority for netclass resolution (higher = higher priority)
color=field(str | None, None), # PCB color (hex like "#FF0000" or CSS name)
)

# Design rules container
DesignRules = record(
constraints=field(Constraints | None, None),
predefined_sizes=field(PredefinedSizes | None, None),
netclasses=field(list | None, None), # List of NetClass objects
)

# Material definition for stackup layers
Material = record(
name=field(str | None, None), # Material name
vendor=field(str | None, None), # Optional vendor
relative_permittivity=field(float | None, None), # Epsilon R (dielectric constant)
loss_tangent=field(float | None, None), # Loss tangent
reference_frequency=field(float | None, None), # Optional frequency in Hz
)

# Copper layer definition for stackup
CopperLayer = record(
thickness=field(float | None, None), # Thickness in mm
role=field(str | None, None), # "signal", "power", "mixed"
)

# Dielectric layer definition for stackup
DielectricLayer = record(
thickness=field(float | None, None), # Thickness in mm
material=field(str | None, None), # Material name (ref to materials list)
form=field(str | None, None), # "core" or "prepreg"
)

# Board stackup configuration
Stackup = record(
materials=field(list[Material] | None, None), # List of Material objects
silk_screen_color=field(str | None, None), # Hex color like "#44805BFF"
solder_mask_color=field(str | None, None), # Hex color like "#191919E6"
thickness=field(float | None, None), # Total thickness in mm
symmetric=field(bool | None, None), # Assert symmetric stackup (default: True)
layers=field(list[CopperLayer | DielectricLayer] | None, None), # Ordered list of stackup layers
copper_finish=field(str | None, None), # Surface finish: "ENIG", "HAL SnPb", "HAL lead-free"
)

# Complete board configuration
BoardConfig = record(
design_rules=field(DesignRules | None, None),
stackup=field(Stackup | None, None), # Board stackup configuration
num_user_layers=field(int, 4), # Number of User.N layers (User.1, User.2, etc.)
)


def deep_merge(base, override):
"""Deep merge two records, with override values taking precedence.

For records: recursively merges fields
For lists: override replaces base entirely
For primitives: override value used if not None
"""

if override == None:
return base
if base == None:
return override

# Handle list types - override replaces entirely
if type(base) == "list":
return override if type(override) == "list" else base

# Check if both are records by checking if they have getattr/type
base_type = type(base)
override_type = type(override)

# Only merge if both are record types (not primitive types)
if base_type == "record" and override_type == "record":
merged = {}

# Get all field names from both records
base_fields = dir(base)
override_fields = dir(override)
all_fields = set(base_fields + override_fields)

for field in all_fields:
if field.startswith("_"): # Skip private fields
continue

base_val = getattr(base, field, None) if field in base_fields else None
override_val = getattr(override, field, None) if field in override_fields else None

if override_val != None:
if base_val != None and type(base_val) != "list":
# Recursively merge if both exist and not lists
merged[field] = deep_merge(base_val, override_val)
else:
# Use override value (including for lists)
merged[field] = override_val
elif base_val != None:
# Keep base value when override is None but base has a value

merged[field] = base_val
else:
# Both are None, keep None
merged[field] = None

# Extract record type from string representation
base_str = str(base)
if base_str.startswith("record["):
type_name = base_str.split("[")[1].split("]")[0]
if type_name == "BoardConfig":
return BoardConfig(**merged)
elif type_name == "DesignRules":
return DesignRules(**merged)
elif type_name == "Constraints":
return Constraints(**merged)
elif type_name == "PredefinedSizes":
return PredefinedSizes(**merged)
elif type_name == "Copper":
return Copper(**merged)
elif type_name == "Holes":
return Holes(**merged)
elif type_name == "Uvias":
return Uvias(**merged)
elif type_name == "Silkscreen":
return Silkscreen(**merged)
elif type_name == "ViaDimension":
return ViaDimension(**merged)
elif type_name == "NetClass":
return NetClass(**merged)

elif type_name == "Stackup":
return Stackup(**merged)
elif type_name == "Material":
return Material(**merged)
elif type_name == "CopperLayer":
return CopperLayer(**merged)
elif type_name == "DielectricLayer":
return DielectricLayer(**merged)
else:
error("Unknown record type for merge: " + type_name)
else:
error("Unable to determine record type from: " + base_str)

# Not records, override wins
return override


def merge_configs(*configs: BoardConfig) -> BoardConfig:
"""Merge multiple BoardConfig objects, with later configs taking precedence.

Args:
*configs: Variable number of BoardConfig objects to merge

Returns:
BoardConfig: Merged configuration with later configs overriding earlier ones
"""
if not configs:
return BoardConfig()

result = configs[0]
for config in configs[1:]:
result = deep_merge(result, config)

return result


def Board(
name: str,
config: BoardConfig,
layout_path: str,
layout_hints: list | None = None,
default: bool = False,
):
builtin.add_board_config(
name=name,
default=default,
config=config,
)
add_property("layout_path", Path(layout_path, allow_not_exist=True))
if layout_hints:
add_property("layout_hints", layout_hints)


# Standard Base Configurations

# Common materials that can be reused across board configurations

# Standard PCB Materials
FR4_CORE = Material(
name="FR4-Core",
relative_permittivity=4.6,
loss_tangent=0.025, # Typical for standard FR4 core
)

BASE_PREPREG = Material(
name="Prepreg",
relative_permittivity=4.4,
loss_tangent=0.025, # Standard prepreg material
)

# Common stackup configurations

# Base 4-layer stackup (1.6mm, 1oz outer/0.5oz inner)
BASE_4L_STACKUP = Stackup(
materials=[FR4_CORE, BASE_PREPREG],
thickness=1.6,
symmetric=True,
copper_finish="HAL SnPb",
silk_screen_color="White",
solder_mask_color="Black",
layers=[
CopperLayer(thickness=0.035, role="mixed"), # Top layer (1oz)
DielectricLayer(thickness=0.21040, material="Prepreg", form="prepreg"),
CopperLayer(thickness=0.0152, role="power"), # Inner L2 (0.5oz)
DielectricLayer(thickness=1.065, material="FR4-Core", form="core"),
CopperLayer(thickness=0.0152, role="power"), # Inner L3 (0.5oz)
DielectricLayer(thickness=0.21040, material="Prepreg", form="prepreg"),
CopperLayer(thickness=0.035, role="mixed"), # Bottom layer (1oz)
],
)

# Common constraint sets for different fabrication capabilities

# Base PCB Constraints (1oz copper)
BASE_CONSTRAINTS = Constraints(
copper=Copper(
minimum_clearance=0.09, # Min spacing between copper features (1oz)
minimum_track_width=0.09, # Min track width (1oz)
minimum_via_diameter=0.25, # Min via diameter (0.25mm)
copper_to_hole_clearance=0.2, # Inner layer via hole to copper clearance
copper_to_edge_clearance=0.3, # Reasonable edge clearance
),
holes=Holes(
minimum_through_hole=0.15, # Min via hole size
hole_to_hole_clearance=0.2, # Via hole-to-hole spacing
),
uvias=Uvias(
minimum_uvia_diameter=0.15, # Micro via diameter (if supported)
minimum_uvia_hole=0.1, # Micro via hole (if supported)
),
silkscreen=Silkscreen(
minimum_item_clearance=0.1, # Reasonable silkscreen clearance
minimum_text_height=0.8, # Reasonable minimum text height
),
)

# Heavy Copper Constraints (2oz copper)
HEAVY_COPPER_CONSTRAINTS = Constraints(
copper=Copper(
minimum_clearance=0.2, # Min spacing (2oz copper)
minimum_track_width=0.16, # Min track width (2oz copper)
minimum_via_diameter=0.25, # Same via diameter
copper_to_hole_clearance=0.2, # Same hole clearance
copper_to_edge_clearance=0.3, # Same edge clearance
),
holes=Holes(
minimum_through_hole=0.15, # Same hole size
hole_to_hole_clearance=0.2, # Same hole spacing
),
uvias=Uvias(
minimum_uvia_diameter=0.15,
minimum_uvia_hole=0.1,
),
silkscreen=Silkscreen(
minimum_item_clearance=0.1,
minimum_text_height=0.8,
),
)

# Common netclass definitions

# Default netclass for general-purpose PCB manufacturing
DEFAULT_NETCLASS = NetClass(
name="Default",
clearance=0.15, # Conservative clearance (different nets)
track_width=0.15, # Safe track width for most manufacturers
via_diameter=0.4, # Standard via size
via_drill=0.2, # Standard drill size
diff_pair_width=0.2, # Reasonable diff pair width
diff_pair_gap=0.2, # Reasonable diff pair gap
)

# Common predefined sizes for design tool dropdowns

# Base predefined sizes for common PCB design
BASE_PREDEFINED_SIZES = PredefinedSizes(
track_widths=[
0.1, # Safe minimum
0.2, # Standard signals
0.4, # Power traces
0.8, # Heavy power
],
via_dimensions=[
ViaDimension(diameter=0.35, drill=0.2), # Small via
ViaDimension(diameter=0.4, drill=0.25), # Standard via
ViaDimension(diameter=0.45, drill=0.3), # Large via
],
)

# Base board configurations that can be extended

# Complete base 4-layer board configuration (1oz outer, 0.5oz inner, base constraints)
BASE_4L = BoardConfig(
design_rules=DesignRules(
constraints=BASE_CONSTRAINTS,
predefined_sizes=BASE_PREDEFINED_SIZES,
netclasses=[DEFAULT_NETCLASS],
),
stackup=BASE_4L_STACKUP,
)