diff --git a/board_config.zen b/board_config.zen new file mode 100644 index 0000000..74ee81e --- /dev/null +++ b/board_config.zen @@ -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; +# +# 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, +)