diff --git a/src/faebryk/exporters/parameters/parameters_to_file.py b/src/faebryk/exporters/parameters/parameters_to_file.py index 4e49f7ae..01a13807 100644 --- a/src/faebryk/exporters/parameters/parameters_to_file.py +++ b/src/faebryk/exporters/parameters/parameters_to_file.py @@ -26,6 +26,8 @@ def export_parameters_to_file(module: Module, path: Path): ] logger.info(f"Writing parameters to {path}") + if not path.exists() or not path.is_file(): + path.touch() if path.suffix == ".txt": with open(path, "w") as f: for module_name, paras in sorted(parameters.items()): diff --git a/src/faebryk/exporters/pcb/kicad/transformer.py b/src/faebryk/exporters/pcb/kicad/transformer.py index 6ac0e658..a0460256 100644 --- a/src/faebryk/exporters/pcb/kicad/transformer.py +++ b/src/faebryk/exporters/pcb/kicad/transformer.py @@ -7,8 +7,9 @@ import uuid from abc import abstractmethod from dataclasses import fields +from enum import Enum, auto from itertools import pairwise -from typing import Any, Callable, Iterable, List, Sequence, TypeVar +from typing import Any, Callable, Iterable, List, Optional, Sequence, TypeVar import numpy as np from shapely import Polygon @@ -19,6 +20,7 @@ from faebryk.core.module import Module from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.node import Node +from faebryk.core.util import get_all_nodes_with_trait from faebryk.libs.geometry.basic import Geometry from faebryk.libs.kicad.fileformats import ( UUID, @@ -32,10 +34,12 @@ C_rect, C_stroke, C_text, + C_text_layer, C_wh, C_xy, C_xyr, C_xyz, + E_fill, ) from faebryk.libs.sexp.dataclass_sexp import dataclass_dfs from faebryk.libs.util import cast_assert, find, get_key @@ -297,9 +301,44 @@ def is_marked(obj) -> bool: # Getter --------------------------------------------------------------------------- @staticmethod - def get_fp(cmp) -> Footprint: + def get_fp(cmp: Node) -> Footprint: return cmp.get_trait(PCB_Transformer.has_linked_kicad_footprint).get_fp() + def get_all_footprints(self) -> List[tuple[Module, Footprint]]: + return [ + (cast_assert(Module, cmp), t.get_fp()) + for cmp, t in get_all_nodes_with_trait( + self.graph, PCB_Transformer.has_linked_kicad_footprint + ) + ] + + # TODO: make universal fp bbox getter (also take into account pads) + def get_footprint_silkscreen_bbox( + self, cmp: Node + ) -> None | tuple[Point2D, Point2D]: + fp = self.get_fp(cmp) + silk_outline = [geo for geo in get_all_geos(fp) if geo.layer == "F.SilkS"] + + extremes = list[C_xy]() + + for geo in silk_outline: + if isinstance(geo, C_line): + extremes.extend([geo.start, geo.end]) + elif isinstance(geo, C_arc): + # TODO: calculate extremes.extend([geo.start, geo.mid, geo.end]) + ... + elif isinstance(geo, C_rect): + extremes.extend([geo.start, geo.end]) + elif isinstance(geo, C_circle): + # TODO: calculate extremes.extend([geo.center, geo.end]) + ... + + if not extremes: + logger.warn(f"{cmp} with fp:{fp.name} has no silk outline") + return None + + return Geometry.bbox([Point2D((point.x, point.y)) for point in extremes]) + def get_net(self, net: FNet) -> Net: nets = {pcb_net.name: pcb_net for pcb_net in self.pcb.nets} return nets[net.get_trait(F.has_overriden_name).get_name()] @@ -543,21 +582,31 @@ def insert_via( ) ) - def insert_text(self, text: str, at: C_xyr, font: Font, front: bool = True): + def insert_text( + self, text: str, at: C_xyr, font: Font, layer: str = "F.SilkS", alignment=None + ): self.pcb.gr_texts.append( GR_Text( text=text, at=at, - layer=f"{'F' if front else 'B'}.SilkS", + layer=layer, effects=C_effects( font=font, justify=( - C_effects.E_justify.center, - C_effects.E_justify.center, - C_effects.E_justify.mirror - if not front - else C_effects.E_justify.normal, - ), + ( + C_effects.E_justify.center, + C_effects.E_justify.center, + C_effects.E_justify.mirror, + ) + if not layer.startswith("F.") + else ( + C_effects.E_justify.center, + C_effects.E_justify.center, + C_effects.E_justify.normal, + ) + ) + if alignment is None + else alignment, ), uuid=self.gen_uuid(mark=True), ) @@ -638,7 +687,13 @@ def get_net_obj_bbox(self, net: Net, layer: str, tolerance=0.0): return Geometry.rect_to_polygon(bbox) - def insert_zone(self, net: Net, layers: str | list[str], polygon: list[Point2D]): + def insert_zone( + self, + net: Net, + layers: str | list[str], + polygon: list[Point2D], + keepout: bool = False, + ): # check if exists zones = self.pcb.zones # TODO check bbox @@ -678,18 +733,81 @@ def insert_zone(self, net: Net, layers: str | list[str], polygon: list[Point2D]) thermal_bridge_width=0.2, smoothing=None, radius=1, - island_removal_mode=Zone.C_fill.E_island_removal_mode.do_not_remove, + # island_removal_mode=Zone.C_fill.E_island_removal_mode.do_not_remove, # noqa E501 island_area_min=10.0, ), locked=False, hatch=Zone.C_hatch(mode=Zone.C_hatch.E_mode.edge, pitch=0.5), priority=0, + keepout=Zone.C_keepout( + tracks=Zone.C_keepout.E_keepout_bool.allowed, + vias=Zone.C_keepout.E_keepout_bool.allowed, + pads=Zone.C_keepout.E_keepout_bool.allowed, + copperpour=Zone.C_keepout.E_keepout_bool.not_allowed, + footprints=Zone.C_keepout.E_keepout_bool.allowed, + ) + if keepout + else None, connect_pads=Zone.C_connect_pads( mode=Zone.C_connect_pads.E_mode.thermal_reliefs, clearance=0.2 ), ) ) + # JLCPCB --------------------------------------------------------------------------- + class JLCPBC_QR_Size(Enum): + SMALL_5x5mm = C_xy(5, 5) + MEDIUM_8x8mm = C_xy(5, 5) + LARGE_10x10mm = C_xy(5, 5) + + def insert_jlcpcb_qr( + self, + size: JLCPBC_QR_Size, + center_at: C_xy, + layer="F.SilkS", + number: bool = True, + ): + assert layer.endswith("SilkS"), "JLCPCB QR code must be on silk screen layer" + if number: + self.insert_text( + "######", + at=C_xyr(center_at.x, center_at.y + size.value.y / 2 + 1, 0), + font=Font(size=C_wh(0.75, 0.75), thickness=0.15), + layer="F.Fab" if layer.startswith("F.") else "B.Fab", + ) + self.insert_geo( + C_rect( + start=C_xy( + center_at.x - size.value.x / 2, center_at.y - size.value.y / 2 + ), + end=C_xy( + center_at.x + size.value.x / 2, center_at.y + size.value.y / 2 + ), + stroke=C_stroke(width=0.15, type=C_stroke.E_type.solid), + fill=E_fill.solid, + layer=layer, + uuid=self.gen_uuid(mark=True), + ) + ) + + def insert_jlcpcb_serial( + self, + center_at: C_xyr, + layer="F.SilkS", + ): + assert layer.endswith( + "SilkS" + ), "JLCPCB serial number must be on silk screen layer" + self.insert_text( + "JLCJLCJLCJLC", + at=center_at, + font=Font( + size=C_wh(1, 1), + thickness=0.15, + ), + layer=layer, + ) + # Positioning ---------------------------------------------------------------------- def move_footprints(self): from faebryk.core.util import get_all_nodes_with_traits @@ -977,3 +1095,64 @@ def set_pcb_outline_complex( self.insert_geo(geo) # find bounding box + + class Side(Enum): + TOP = auto() + BOTTOM = auto() + LEFT = auto() + RIGHT = auto() + + def set_designator_position( + self, + offset: float, + displacement: C_xy = C_xy(0, 0), + rotation: Optional[float] = None, + offset_side: Side = Side.BOTTOM, + layer: Optional[C_text_layer] = None, + font: Optional[Font] = None, + knockout: Optional[C_text_layer.E_knockout] = None, + justify: Optional[ + tuple[C_effects.E_justify, C_effects.E_justify, C_effects.E_justify] + ] = None, + ): + for mod, fp in self.get_all_footprints(): + reference = fp.propertys["Reference"] + reference.layer = ( + layer + if layer + else C_text_layer( + layer="F.SilkS" if fp.layer.startswith("F") else "B.SilkS" + ) + ) + reference.layer.knockout = ( + knockout if knockout else reference.layer.knockout + ) + reference.effects.font = font if font else reference.effects.font + + reference.effects.justify = ( + justify if justify else reference.effects.justify + ) + rot = rotation if rotation else reference.at.r + + footprint_bbox = self.get_footprint_silkscreen_bbox(mod) + if not footprint_bbox: + continue + max_coord = C_xy(*footprint_bbox[1]) + min_coord = C_xy(*footprint_bbox[0]) + + if offset_side == self.Side.BOTTOM: + reference.at = C_xyr( + displacement.x, max_coord.y + offset - displacement.y, rot + ) + elif offset_side == self.Side.TOP: + reference.at = C_xyr( + displacement.x, min_coord.y - offset - displacement.y, rot + ) + elif offset_side == self.Side.LEFT: + reference.at = C_xyr( + min_coord.x - offset - displacement.x, displacement.y, rot + ) + elif offset_side == self.Side.RIGHT: + reference.at = C_xyr( + max_coord.x + offset + displacement.x, displacement.y, rot + ) diff --git a/src/faebryk/exporters/pcb/layout/extrude.py b/src/faebryk/exporters/pcb/layout/extrude.py index f3ce1265..d88ee387 100644 --- a/src/faebryk/exporters/pcb/layout/extrude.py +++ b/src/faebryk/exporters/pcb/layout/extrude.py @@ -29,6 +29,7 @@ class LayoutExtrude(Layout): (0, 0, 0, F.has_pcb_position.layer_type.NONE) ) dynamic_rotation: bool = False + reverse_order: bool = False def apply(self, *node: Node): """ @@ -40,7 +41,15 @@ def apply(self, *node: Node): vector = self.vector if len(self.vector) == 3 else (*self.vector, 0) - for i, n in enumerate(node): + for i, n in enumerate( + sorted( + node, + key=lambda n: n.get_trait(F.has_designator).get_designator() + if n.has_trait(F.has_designator) + else n.get_full_name(), + reverse=self.reverse_order, + ) + ): vec_i = ( vector[0] * i, vector[1] * i, diff --git a/src/faebryk/library/ESP32_C3_MINI_1.py b/src/faebryk/library/ESP32_C3_MINI_1.py index 07ab53aa..f2c30369 100644 --- a/src/faebryk/library/ESP32_C3_MINI_1.py +++ b/src/faebryk/library/ESP32_C3_MINI_1.py @@ -75,8 +75,8 @@ def attach_to_footprint(self): "27": self.gpio[19].signal, # 28 is not connected # 29 is not connected - "30": self.uart.rx.signal, - "31": self.uart.tx.signal, + "30": self.gpio[20].signal, # self.uart.rx.signal, + "31": self.gpio[21].signal, # self.uart.tx.signal, # 32 is not connected # 33 is not connected # 34 is not connected @@ -114,5 +114,5 @@ def attach_to_footprint(self): designator_prefix = L.f_field(F.has_designator_prefix_defined)("U") datasheet = L.f_field(F.has_datasheet_defined)( - "https://www.espressif.com/sites/default/files/russianDocumentation/esp32-c3-mini-1_datasheet_en.pdf" + "https://www.espressif.com/sites/default/files/documentation/esp32-c3-mini-1_datasheet_en.pdf" ) diff --git a/src/faebryk/library/ESP32_C3_MINI_1_Reference_Design.py b/src/faebryk/library/ESP32_C3_MINI_1_Reference_Design.py index ea312996..60adb880 100644 --- a/src/faebryk/library/ESP32_C3_MINI_1_Reference_Design.py +++ b/src/faebryk/library/ESP32_C3_MINI_1_Reference_Design.py @@ -45,7 +45,34 @@ def __preinit__(self): # TODO: set the following in the pinmux # jtag gpio 4,5,6,7 - # USB gpio 18,19 + self.esp32_c3_mini_1.esp32_c3.usb.usb_if.d.n.connect( + self.esp32_c3_mini_1.esp32_c3.gpio[18].signal + ) + self.esp32_c3_mini_1.esp32_c3.usb.usb_if.d.p.connect( + self.esp32_c3_mini_1.esp32_c3.gpio[19].signal + ) + # UART0 gpio 30/31 (default) + self.esp32_c3_mini_1.esp32_c3.uart[0].rx.connect( + self.esp32_c3_mini_1.esp32_c3.gpio[20] + ) + self.esp32_c3_mini_1.esp32_c3.uart[0].tx.connect( + self.esp32_c3_mini_1.esp32_c3.gpio[21] + ) + + # UART1 gpio 8/9 + self.esp32_c3_mini_1.esp32_c3.uart[1].rx.connect( + self.esp32_c3_mini_1.esp32_c3.gpio[8] + ) + self.esp32_c3_mini_1.esp32_c3.uart[1].tx.connect( + self.esp32_c3_mini_1.esp32_c3.gpio[7] + ) + # i2c + self.esp32_c3_mini_1.esp32_c3.i2c.sda.connect( + self.esp32_c3_mini_1.esp32_c3.gpio[3] # default 21 + ) + self.esp32_c3_mini_1.esp32_c3.i2c.scl.connect( + self.esp32_c3_mini_1.esp32_c3.gpio[2] # default 22 + ) # connect USB self.usb.connect(self.esp32_c3_mini_1.esp32_c3.usb) diff --git a/src/faebryk/library/ElectricLogic.py b/src/faebryk/library/ElectricLogic.py index 759e7a82..bcabf40b 100644 --- a/src/faebryk/library/ElectricLogic.py +++ b/src/faebryk/library/ElectricLogic.py @@ -5,6 +5,8 @@ from enum import Enum, auto from typing import Iterable, Self +from deprecated import deprecated + import faebryk.library._F as F from faebryk.core.graphinterface import GraphInterface from faebryk.core.link import LinkFilteredException, _TLinkDirectShallow @@ -203,6 +205,8 @@ def connect_via_bridge( return target.connect_via(bridge, self.signal) return self.signal.connect_via(bridge, target) + # TODO: remove + @deprecated("Use connect with linkcls") def connect_shallow( self, other: Self, @@ -222,3 +226,28 @@ def connect_shallow( self.reference.lv.connect(other.reference.lv) return super().connect_shallow(other) + + def _on_connect(self, other: ModuleInterface): + super()._on_connect(other) + + if isinstance(other, ElectricLogic): + # get the pulls from the other and self + if other.has_trait(ElectricLogic.has_pulls) and self.has_trait( + ElectricLogic.has_pulls + ): + pulls_other = other.get_trait(ElectricLogic.has_pulls).get_pulls() + pulls_self = self.get_trait(ElectricLogic.has_pulls).get_pulls() + + # asser if both pull up and down are set + assert not ( + pulls_other[0] and pulls_self[1] + ), "Both pull up and down are set in the same connection" + assert not ( + pulls_other[1] and pulls_self[0] + ), "Both pull up and down are set in the same connection" + + # if there are 2 pulls, specialize into 1 new pull + if pulls_other[0] and pulls_self[0]: + assert NotImplementedError(), "Merging pull up not implemented" + if pulls_other[1] and pulls_self[1]: + assert NotImplementedError(), "Merging pull down not implemented" diff --git a/src/faebryk/library/ME6211C33M5G_N.py b/src/faebryk/library/ME6211C33M5G_N.py index 8d81ebd0..3c51aa11 100644 --- a/src/faebryk/library/ME6211C33M5G_N.py +++ b/src/faebryk/library/ME6211C33M5G_N.py @@ -32,7 +32,7 @@ def attach_to_footprint(self): { "1": self.power_in.hv, "2": self.power_in.lv, - "3": self.enable, + "3": self.enable.signal, "5": self.power_out.hv, } ) diff --git a/src/faebryk/library/Mounting_Hole.py b/src/faebryk/library/Mounting_Hole.py index 2babd569..f3389319 100644 --- a/src/faebryk/library/Mounting_Hole.py +++ b/src/faebryk/library/Mounting_Hole.py @@ -2,32 +2,50 @@ # SPDX-License-Identifier: MIT import logging +from enum import Enum, StrEnum import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L -from faebryk.libs.units import P, Quantity logger = logging.getLogger(__name__) class Mounting_Hole(Module): - diameter: F.TBD[Quantity] + class MetricDiameter(Enum): + M2 = ("2.2mm", "_M2") + M3 = ("3.2mm", "_M3") + M4 = ("4.3mm", "_M4") + M5 = ("5.3mm", "_M5") + M6 = ("6.4mm", "_M6") + M8 = ("8.4mm", "_M8") + + class PadType(StrEnum): + NoPad = "" + Pad = "_Pad" + TopBottom = "_TopBottom" + TopOnly = "_TopOnly" + Via = "_Via" + + diameter: MetricDiameter + pad_type: PadType attach_to_footprint: F.can_attach_to_footprint_symmetrically designator_prefix = L.f_field(F.has_designator_prefix_defined)("H") def __preinit__(self): - # Only 3.2mm supported for now - self.diameter.merge(F.Constant(3.2 * P.mm)) - - # footprint = L.f_field(F.has_footprint_defined)( - # F.KicadFootprint("MountingHole:MountingHole_3.2mm_M3_Pad", pin_names=[]) - # ) - - # TODO make back to f_field, rt_field because of imports - @L.rt_field - def footprint(self): - return F.has_footprint_defined( - F.KicadFootprint("MountingHole:MountingHole_3.2mm_M3_Pad", pin_names=[]) - ) + # footprint = L.f_field(F.has_footprint_defined)( + # F.KicadFootprint("MountingHole:MountingHole_3.2mm_M3_Pad", pin_names=[]) + # ) + + # TODO make back to f_field, rt_field because of imports + @L.rt_field + def footprint(self): + assert self.diameter is not None + assert self.pad_type is not None + return F.has_footprint_defined( + F.KicadFootprint( + f"MountingHole:MountingHole_{self.diameter.value[0]}{self.diameter.value[1] if self.pad_type.value != self.PadType.NoPad else ''}{self.pad_type.value}", # noqa E501 + pin_names=[], + ) + ) diff --git a/src/faebryk/library/Power.py b/src/faebryk/library/Power.py index abf48d0a..594a3c39 100644 --- a/src/faebryk/library/Power.py +++ b/src/faebryk/library/Power.py @@ -11,10 +11,18 @@ class is_power_source(ModuleInterface.TraitT): ... class is_power_source_defined(is_power_source.impl()): ... + class is_power_sink(ModuleInterface.TraitT): ... + + class is_power_sink_defined(is_power_sink.impl()): ... + def make_source(self): self.add(self.is_power_source_defined()) return self + def make_sink(self): + self.add(self.is_power_sink_defined()) + return self + def _on_connect(self, other: "Power"): if self.has_trait(self.is_power_source) and other.has_trait( self.is_power_source diff --git a/src/faebryk/library/RP2040.py b/src/faebryk/library/RP2040.py index de4f6323..246e343b 100644 --- a/src/faebryk/library/RP2040.py +++ b/src/faebryk/library/RP2040.py @@ -32,28 +32,26 @@ class RP2040(Module): uart: F.UART_Base def __preinit__(self): - # TODO - return # decouple power rails and connect GNDs toghether - gnd = self.io_vdd.lv for pwrrail in [ self.io_vdd, self.adc_vdd, self.core_vdd, self.vreg_in, - self.vreg_out, self.usb.usb_if.buspower, ]: - pwrrail.lv.connect(gnd) pwrrail.decoupled.decouple() + pwrrail.make_sink() + self.vreg_out.decoupled.decouple() + self.vreg_out.make_source() # set parameters self.vreg_out.voltage.merge(1.1 * P.V) - self.io_vdd.voltage.merge(3.3 * P.V) + self.io_vdd.voltage.merge(F.Range(1.8 * P.V, 3.63 * P.V)) F.ElectricLogic.connect_all_node_references( self.get_children(direct_only=True, types=ModuleInterface).difference( - {self.adc_vdd, self.core_vdd} + {self.adc_vdd, self.core_vdd, self.vreg_out} ) ) diff --git a/src/faebryk/library/USB_C_PSU_Vertical.py b/src/faebryk/library/USB_C_PSU_Vertical.py index 02626e07..85487513 100644 --- a/src/faebryk/library/USB_C_PSU_Vertical.py +++ b/src/faebryk/library/USB_C_PSU_Vertical.py @@ -53,8 +53,8 @@ def __preinit__(self): ) # configure as ufp with 5V@max3A - self.usb_connector.cc1.connect_via(self.configuration_resistors[0], gnd) - self.usb_connector.cc2.connect_via(self.configuration_resistors[1], gnd) + self.usb_connector.cc1.connect_via(self.configuration_resistors[1], gnd) + self.usb_connector.cc2.connect_via(self.configuration_resistors[0], gnd) # EMI shielding self.usb_connector.shield.connect_via(self.gnd_resistor, gnd) diff --git a/src/faebryk/libs/kicad/fileformats.py b/src/faebryk/libs/kicad/fileformats.py index a6e25501..4e492707 100644 --- a/src/faebryk/libs/kicad/fileformats.py +++ b/src/faebryk/libs/kicad/fileformats.py @@ -962,6 +962,9 @@ class E_keepout_bool(SymEnum): @dataclass(kw_only=True) class C_filled_polygon: layer: str + island: Optional[bool] = field( + **sexp_field(positional=True), default=None + ) pts: C_polygon.C_pts net: int