Files
diodeinc__pcb/examples/board_config.zen
Akhil Velagapudi 78b63cd9c1 [ENG-102] Auto assign netclasses by matching impedances (#232)
* Sync units, board_config

Signed-off-by: akhilles <akhilvelagapudi@gmail.com>

* Parse netclasses

Signed-off-by: akhilles <akhilvelagapudi@gmail.com>

* Auto assign netclasses by matching impedances

Signed-off-by: akhilles <akhilvelagapudi@gmail.com>

* Only match net -> SE, DiffPair -> diff

Signed-off-by: akhilles <akhilvelagapudi@gmail.com>

---------

Signed-off-by: akhilles <akhilvelagapudi@gmail.com>
2025-10-15 09:45:47 -04:00

693 lines
24 KiB
Python

load("units.zen", "Impedance")
# 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)
single_ended_impedance=field(Impedance | None, None),
differential_pair_impedance=field(Impedance | None, None),
)
# 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
# Default soldermask thickness assumptions (not currently configurable):
# - On FR-4: 1.2 mil (0.03048 mm)
# - On Copper: 0.6 mil (0.01524 mm)
# - Between Traces: 1.2 mil (0.03048 mm)
# - Dielectric Constant: 3.8
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.)
spec=field(str | None, None), # Path to a specification file
)
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,
layout_path: str,
config: BoardConfig | None = None,
layers: int | None = None,
layout_hints: list | None = None,
default: bool = False,
):
# If config is not provided, construct it from layers
if config == None:
if layers == None:
error("Either 'config' or 'layers' must be provided")
elif layers == 2:
stackup = BASE_2L_STACKUP
netclasses = BASE_2L_NETCLASSES
elif layers == 4:
stackup = BASE_4L_STACKUP
netclasses = BASE_4L_NETCLASSES
elif layers == 6:
stackup = BASE_6L_STACKUP
netclasses = BASE_6L_NETCLASSES
elif layers == 8:
stackup = BASE_8L_STACKUP
netclasses = BASE_8L_NETCLASSES
else:
error("layers must be 2, 4, 6, or 8")
design_rules = DesignRules(
constraints=BASE_CONSTRAINTS,
predefined_sizes=BASE_PREDEFINED_SIZES,
netclasses=netclasses,
)
config = BoardConfig(
design_rules=design_rules,
stackup=stackup,
)
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
# 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.5, # Standard via size (5.3:1 aspect ratio on 1.6mm board)
via_drill=0.3, # Standard drill size
diff_pair_width=0.2, # Reasonable diff pair width
diff_pair_gap=0.2, # Reasonable diff pair gap
)
# 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
)
# Stackup Design Philosophy
#
# Layer convention for 4L/6L/8L stackups:
# - First 3 layers: SIG/GND/PWR (always)
# - Last layer: SIG (always)
#
# This enables module reusability - a 4L module can be ported to 6L/8L by using the
# same top 3 layers and bottom layer. Internal layers provide additional routing.
#
# Impedance Control:
# - GCPW (Grounded Coplanar Waveguide) for all impedance-controlled routing
# - L1 (top) is primary impedance-controlled layer
# - L6/L8 (bottom) serve as backup impedance-controlled layers on 6L/8L
# - 8L also provides L6 as symmetric stripline (GND/SIG/GND)
#
# Selection:
# - 2L: Simple, low speed
# - 4L: Standard, L1 impedance-controlled
# - 6L: More routing, L1 + L6 impedance-controlled
# - 8L: High density, L1 + L6 (stripline) + L8 impedance-controlled
# Common stackup configurations
# Base 2-layer stackup (1.6mm, 1oz outer)
# SIG/SIG
# Use case: Simple designs without dedicated power/ground planes
BASE_2L_STACKUP = Stackup(
materials=[FR4_CORE],
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=1.53, material="FR4-Core", form="core"),
CopperLayer(thickness=0.035, role="mixed"), # Bottom layer (1oz)
],
)
BASE_2L_NETCLASSES = [DEFAULT_NETCLASS]
# Base 4-layer stackup (1.6mm, 1oz outer/0.5oz inner)
# SIG/GND/PWR/SIG
#
# Layers:
# L1: Mixed + impedance-controlled (GCPW, ref L2, 0.21mm coupling)
# L2: GND plane
# L3: PWR plane
# L4: Mixed signal
#
# Use: Standard stackup with dedicated GND/PWR planes. L1 for high-speed routing.
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="ground"), # 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)
],
)
# 4L Impedance-Controlled Netclasses (L1 GCPW, 0.21mm dielectric, 0.2mm coplanar gap)
# Calculated using Sierra Circuits impedance calculator
BASE_4L_NETCLASSES = [
DEFAULT_NETCLASS,
NetClass(
name="50Ohm SE",
clearance=0.2,
track_width=0.2908,
color="#0000C2FF",
single_ended_impedance=Impedance(50),
),
NetClass(
name="85Ohm Diff",
clearance=0.2,
track_width=0.2235,
diff_pair_width=0.2235,
diff_pair_gap=0.127,
color="#C200C2FF",
differential_pair_impedance=Impedance(85),
),
NetClass(
name="90Ohm Diff",
clearance=0.2,
track_width=0.1867,
diff_pair_width=0.1867,
diff_pair_gap=0.127,
color="#00C200FF",
differential_pair_impedance=Impedance(90),
),
NetClass(
name="100Ohm Diff",
clearance=0.2,
track_width=0.1334,
diff_pair_width=0.1334,
diff_pair_gap=0.127,
color="#C2C200FF",
differential_pair_impedance=Impedance(100),
),
]
# Base 6-layer stackup (1.6mm, 1oz outer/0.5oz inner)
# SIG/GND/PWR/SIG/GND/SIG
#
# Layers:
# L1: Primary impedance-controlled (GCPW, ref L2, 0.099mm coupling)
# L2: GND plane
# L3: PWR plane
# L4: Mixed signal
# L5: GND plane
# L6: Backup impedance-controlled (GCPW, ref L5, 0.099mm coupling)
#
# Use: More routing space than 4L. Two ground planes for return paths.
BASE_6L_STACKUP = Stackup(
materials=[
FR4_CORE,
Material(
name="2116",
relative_permittivity=4.16,
loss_tangent=0.025,
),
Material(
name="3313",
relative_permittivity=4.1,
loss_tangent=0.025,
),
],
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.09940, material="3313", form="prepreg"),
CopperLayer(thickness=0.0152, role="ground"), # Inner L2 (0.5oz)
DielectricLayer(thickness=0.55, material="FR4-Core", form="core"),
CopperLayer(thickness=0.0152, role="power"), # Inner L3 (0.5oz)
DielectricLayer(thickness=0.11640, material="2116", form="prepreg"),
CopperLayer(thickness=0.0152, role="mixed"), # Inner L4 (0.5oz)
DielectricLayer(thickness=0.55, material="FR4-Core", form="core"),
CopperLayer(thickness=0.0152, role="ground"), # Inner L5 (0.5oz)
DielectricLayer(thickness=0.09940, material="3313", form="prepreg"),
CopperLayer(thickness=0.035, role="mixed"), # Bottom layer (1oz)
],
)
# 6L Impedance-Controlled Netclasses (L1 GCPW, 0.099mm dielectric, 0.2mm coplanar gap)
# Calculated using Sierra Circuits impedance calculator
BASE_6L_NETCLASSES = [
DEFAULT_NETCLASS,
NetClass(
name="50Ohm SE",
clearance=0.2,
track_width=0.1461,
color="#0000C2FF",
single_ended_impedance=Impedance(50),
),
NetClass(
name="85Ohm Diff",
clearance=0.2,
track_width=0.141,
diff_pair_width=0.141,
diff_pair_gap=0.127,
color="#C200C2FF",
differential_pair_impedance=Impedance(85),
),
NetClass(
name="90Ohm Diff",
clearance=0.2,
track_width=0.1359,
diff_pair_width=0.1359,
diff_pair_gap=0.1524,
color="#00C200FF",
differential_pair_impedance=Impedance(90),
),
NetClass(
name="100Ohm Diff",
clearance=0.2,
track_width=0.127,
diff_pair_width=0.127,
diff_pair_gap=0.2286,
color="#C2C200FF",
differential_pair_impedance=Impedance(100),
),
]
# Base 8-layer stackup (1.6mm, 1oz outer/0.5oz inner)
# SIG/GND/PWR/SIG/GND/SIG/GND/SIG
#
# Layers:
# L1: Primary impedance-controlled (GCPW, ref L2, 0.116mm coupling)
# L2: GND plane
# L3: PWR plane
# L4: Mixed signal
# L5: GND plane
# L6: Secondary impedance-controlled (symmetric stripline, L5/L7 refs)
# L7: GND plane
# L8: Backup impedance-controlled (GCPW, ref L7)
#
# Use: High routing density. Three ground planes. L6 symmetric stripline for EMI control.
BASE_8L_STACKUP = Stackup(
materials=[
FR4_CORE,
Material(
name="2116",
relative_permittivity=4.16,
loss_tangent=0.025,
),
Material(
name="1080",
relative_permittivity=3.91,
loss_tangent=0.025,
),
],
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.1164, material="2116", form="prepreg"),
CopperLayer(thickness=0.0152, role="ground"), # Inner L2 (0.5oz)
DielectricLayer(thickness=0.3, material="FR4-Core", form="core"),
CopperLayer(thickness=0.0152, role="power"), # Inner L3 (0.5oz)
DielectricLayer(thickness=0.0764, material="1080", form="prepreg"),
DielectricLayer(thickness=0.0764, material="1080", form="prepreg"),
CopperLayer(thickness=0.0152, role="mixed"), # Inner L4 (0.5oz)
DielectricLayer(thickness=0.3, material="FR4-Core", form="core"),
CopperLayer(thickness=0.0152, role="ground"), # Inner L5 (0.5oz)
DielectricLayer(thickness=0.0764, material="1080", form="prepreg"),
DielectricLayer(thickness=0.0764, material="1080", form="prepreg"),
CopperLayer(thickness=0.0152, role="mixed"), # Inner L6 (0.5oz)
DielectricLayer(thickness=0.3, material="FR4-Core", form="core"),
CopperLayer(thickness=0.0152, role="ground"), # Inner L7 (0.5oz)
DielectricLayer(thickness=0.1164, material="2116", form="prepreg"),
CopperLayer(thickness=0.035, role="mixed"), # Bottom layer (1oz)
],
)
# 8L Impedance-Controlled Netclasses (L1 GCPW, 0.1164mm dielectric, 0.2mm coplanar gap)
# Calculated using Sierra Circuits impedance calculator
# Note: These geometries also work on L6 (asymmetric stripline) with acceptable tolerance:
# 50Ohm SE → 48.5Ohm (-3%), 85Ohm Diff → 83.5Ohm (-1.7%), 90Ohm Diff → 88.0Ohm (-2.2%), 100Ohm Diff → 97.6Ohm (-2.4%)
# Worst case is still within reasonable bounds for most applications.
BASE_8L_NETCLASSES = [
DEFAULT_NETCLASS,
NetClass(
name="50Ohm SE",
clearance=0.2,
track_width=0.1702,
color="#0000C2FF",
single_ended_impedance=Impedance(50),
),
NetClass(
name="85Ohm Diff",
clearance=0.2,
track_width=0.1588,
diff_pair_width=0.1588,
diff_pair_gap=0.127,
color="#C200C2FF",
differential_pair_impedance=Impedance(85),
),
NetClass(
name="90Ohm Diff",
clearance=0.2,
track_width=0.1384,
diff_pair_width=0.1384,
diff_pair_gap=0.127,
color="#00C200FF",
differential_pair_impedance=Impedance(90),
),
NetClass(
name="100Ohm Diff",
clearance=0.2,
track_width=0.127,
diff_pair_width=0.127,
diff_pair_gap=0.1778,
color="#C2C200FF",
differential_pair_impedance=Impedance(100),
),
]
# 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 predefined sizes for design tool dropdowns
# Base predefined sizes for common PCB design
BASE_PREDEFINED_SIZES = PredefinedSizes(
track_widths=[
0.127,
0.2,
0.4,
0.8,
],
via_dimensions=[
ViaDimension(diameter=0.4, drill=0.2),
ViaDimension(diameter=0.5, drill=0.3),
],
)
# Base board configurations that can be extended
BASE_DESIGN_RULES = DesignRules(
constraints=BASE_CONSTRAINTS,
predefined_sizes=BASE_PREDEFINED_SIZES,
netclasses=[DEFAULT_NETCLASS],
)
# Complete base 4-layer board configuration (1oz outer, 0.5oz inner, base constraints)
BASE_4L = BoardConfig(
design_rules=BASE_DESIGN_RULES,
stackup=BASE_4L_STACKUP,
)