diff --git a/src/design.stanza b/src/design.stanza index 096a8ee4..0142a588 100644 --- a/src/design.stanza +++ b/src/design.stanza @@ -4,4 +4,5 @@ defpackage jsl/design: forward jsl/design/E-Series forward jsl/design/introspection forward jsl/design/settings - forward jsl/design/solvers \ No newline at end of file + forward jsl/design/solvers + forward jsl/design/Substrate diff --git a/src/design/Substrate.stanza b/src/design/Substrate.stanza new file mode 100644 index 00000000..128f47a5 --- /dev/null +++ b/src/design/Substrate.stanza @@ -0,0 +1,384 @@ +#use-added-syntax(jitx) +defpackage jsl/design/Substrate: + import core + import collections + import jitx + import jitx/commands + + import maybe-utils + + import jsl/ensure + import jsl/errors + import jsl/layerstack + +doc: \ +Interface type for Substrate Definitions + +The idea behind this type is to provide a consistent +interface that all fabricator specifc substrates can follow. +This will allow a design to swap out the current substrate for +a new substrate with minimal disruption to the design. + +public deftype ISubstrate + +doc: \ +Stackup Generator for this Substrate + +public defmulti stackup (f:ISubstrate) -> LayerStack + +doc: \ +Get the vias registered with this substrate. + +This method is useful for constructing a `pcb-board` +definition from the substrate. + +public defmulti vias (f:ISubstrate) -> Tuple + +doc: \ +Create a Single-Ended Routing Structure for this Substrate + +This method returns a `pcb-routing-structure` instance for +a given characteristic impedance. + +@param f Substrate object +@param imped Characteristic impedance of desired routing structure +@param neckdown Specify a custom neckdown property for this +routing structure to override the default. For example, you +might need smaller clearance near a component. + +public defmulti se-routing-struct ( + f:ISubstrate, + imped:Int + -- + neckdown:NeckDown = ? + ) -> RoutingStructure + +doc: \ +Create a Differential Routing Structure for this Substrate + +This method returns a `pcb-differential-routing-structure` instance for +a given characteristic impedance. + +@param f Substrate object +@param imped Characteristic impedance of desired routing structure +@param uncoupled Specify an optional routing structure for the +uncoupled region of a differential pair. By default, the uncoupled +region keeps the same features as the diff-pair. User can override +that behavior with this value. +@param neckdown Specify an optional custom differential neckdown property +for this routing structure to override the default. For example, you +might need smaller clearance near a component. + +public defmulti diff-routing-struct ( + f:ISubstrate, + imped:Int + -- + uncoupled:RoutingStructure = ? + neckdown:DifferentialNeckDown = ? + ) -> DifferentialRoutingStructure + + + +doc: \ +Lookup-based Substrate Type + +This type is used to define board specific features that +are unique to a particular manufacturer, process, etc. + +The idea is that some fabricators (like JLC-PCB) define +standard features like via sizes, trace impedances, +and stackups to make it easier for users to purchase +inexpensive boards. + + +public defstruct Substrate <: ISubstrate : + doc: \ + Layer Stackup for the PCB + + This defines the dielectric and copper stackup + for the PCB. + + stackup:LayerStack with: + as-method => true + + doc: \ + Via Set Available for this Substrate. + This collection contains the defined vias for + this design. + + This collection is immutable because it must be + passed to the `pcb-board` creation via {@link make-board-def} + + vias:Tuple with: + as-method => true + doc: \ + Single-Ended Routing Structure Generators + This object is not intended to be accessed directly. + User should access this type using {@link se-routing-struct} + + se:HashTable -> RoutingStructure)> + doc: \ + Differential Routing Structure Generators + This object is not intended to be accessed directly. + User should access this type using {@link diff-routing-struct} + + df:HashTable, Maybe) -> DifferentialRoutingStructure)> +with: + printer => true + keyword-constructor => true + +doc: \ +Constructor for Substrate + +@param stackup PCB Stackup Construction Definition +@param vias Collection of Vias for the design to use. +@param single-ended Collection of generator functions categorized +by characteristic impedance +@param differential Collection of gneerator functions categorized +by characteristic impedance + +public defn Substrate ( + -- + stackup:LayerStack, + vias:Collection, + single-ended:Collection -> RoutingStructure)>>, + differential:Collection, Maybe) -> DifferentialRoutingStructure)>> + ) -> Substrate: + Substrate( + stackup = stackup, + vias = to-tuple $ vias, + se = to-hashtable -> RoutingStructure)>(single-ended), + df = to-hashtable, Maybe) -> DifferentialRoutingStructure)>(differential) + ) + +doc: \ +Single-Ended Routing Structure Creator + +User selects a routing structure by characteristic impedance +that is supported by this substrate. The substrate must have +been previously initialized with a substrate that supports +this impedance. + +@param f Board Substrate +@param imped Characteristic impedance in ohms. +@param neckdown Optional neckdown feature for the +created routing structure. This allows the user to +customize the behavior of routes near their endpoints. +@return Single-Ended Routing Structure for the passed +characteristic impedance. +@throws ValueError if no routing structure for that characteristic +impedance exists. + +public defmethod se-routing-struct ( + f:Substrate, + imped:Int + -- + neckdown:NeckDown = ? + ) -> RoutingStructure: + val func? = get?(se(f), imped) + val func = match(func?): + (_:False): + throw $ ValueError("No Single-Ended Impedance Registered for Characteristic Impedance '%_'" % [imped]) + (x): x + func(neckdown) + +doc: \ +Differential Routing Structure Creator + +User selects a differential routing structure +by characteristic impedance. + +@param f Board Substrate +@param imped Characteristic impedance in ohms. +@param uncoupled Optional single-ended uncoupled region +routing structure. Typically, the substrate has a +sensible default, like the same width at the individual +trace widths of the differential pair. +@param neckdown Optional differential neckdown feature for the +created routing structure. This allows the user to +customize the behavior of routes near their endpoints. +@return Differential Routing Structure for the passed +characteristic impedance. +@throws ValueError if no routing structure for that characteristic +impedance exists. + +public defmethod diff-routing-struct ( + f:Substrate, + imped:Int + -- + uncoupled:RoutingStructure = ? + neckdown:DifferentialNeckDown = ? + ) -> DifferentialRoutingStructure: + val func? = get?(df(f), imped) + val func = match(func?): + (_:False): + throw $ ValueError("No Differential Impedance Registered for Characteristic Impedance '%_'" % [imped]) + (x): x + func(uncoupled, neckdown) + + +doc: \ +Retrieve a via from the substrate by name +@param f Substrate +@param name Name of the via +@return Via matching the given name. +@throws ValueError if no via by that name has been registered +with the substrate. + +public defn get-via (f:Substrate, n:String) -> Via : + val ret? = for v in vias(f) first: + if name(v) == n: + One(v) + else: + None() + match(ret?): + (_:None): + throw $ ValueError("No Via with name '%_' defined" % [name]) + (given:One): value(given) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Utilities +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +doc: \ +Generator for the `uncoupled` statement + +This function expects to be called from a `pcb-differential-routing-structure` +context. +@param default Default uncoupled region value is `uncoupled?` is `None()` +@param uncoupled? Override for the default routing structure of the uncoupled region. + +public defn make-uncoupled-region (default:RoutingStructure, uncoupled?:Maybe = None()): + inside pcb-differential-routing-structure: + val uncoupled-spec = match(uncoupled?): + (_:None): default + (given:One): value(given) + + uncoupled-region = uncoupled-spec + +doc: \ +Convert a Substrate into a `pcb-board` definition +@param f Substrate +@param outline Shape for the resulting `pcb-board` +@param signal-shrink Optional parameter that defines the +signal boundary in one of two ways: + +1. If a `Double` - then the `outline` parameter is shrunk by this amount in mm. This +value must be positive. +2. If a `Shape` - then this shape is applied to the `pcb-board` directly. + +@return `pcb-board` definition. + +public defn make-board-def (f:Substrate, outline:Shape -- signal-shrink:Maybe = None()) -> Board: + pcb-board board-def-pcb: + stackup = create-pcb-stackup $ stackup(f) + boundary = outline + match(signal-shrink): + (_:None): false + (given:One): + val shrink = value(given) + ensure-positive!("signal-shrink", shrink) + signal-boundary = expand(outline, (- shrink)) + (given:One): + signal-boundary = value(given) + vias = vias(f) + board-def-pcb + +defn check-layer ( + f:Substrate, + v:Via, + start:Maybe + stop:Maybe + ) -> True|False: + val ls = stackup(f) + val sl? = map(start, to-layer-index) + val el? = map(stop, to-layer-index) + val s = via-start(v) + val e = via-stop(v) + match(sl?, el?): + (sl:None, el:None): + true + (sl:One, el:None): + in-range?(ls, value(sl), s, e) + (sl:None, el:One): + in-range?(ls, value(el), s, e) + (sl:One, el:One): + in-range?(ls, value(sl), s, e) and in-range?(ls, value(el), s, e) + +defn check-type ( + v:Via, + type?:Maybe + ) -> True|False : + match(type?): + (_:None): true + (given:One): + value(given) == via-type(v) + +defn check-boolean (v:Via, spec?:Maybe, func:(Via -> True|False)) -> True|False : + match(spec?): + (_:None): true + (given:One): + value(given) == func(v) + +val check-filled = check-boolean{_0, _1, via-filled} +val check-via-in-pad = check-boolean{_0, _1, via-in-pad} + +defn check-tented (v:Via, tented?:Maybe) -> True|False : + match(tented?): + (_:None): true + (given:One): + value(given) == via-tented(v) + +defn check-backdrill (f:Substrate, v:Via, backdrill?:Maybe) -> True|False : + match(backdrill?): + (_:None): true + (given:One): + value(given) == (via-backdrill(v) is-not False) + +doc: \ +Query for a via defined in this substrate + +This allows the user to query for a via that +matches some set of requirements from the substrate. +This allows us to avoid using explicit via definitions +in code for easier swapping of substrates. + +If a query parameter is not provided, then it defaults +to `None()` and it will not affect the resulting via set. + +@param f Substrate to query +@param start Copper layer for the start of the via +@param stop Copper layer for the end of the via +@param type Mechanical vs Laser Selector +@param filled Selects for filled state +@param tented Selects for tented state +@param via-in-pad Selects for Via-in-Pad eligibility +@param backdrill Selects for any via that is configured +for a backdrill. + +public defn query-via ( + f:Substrate -- + start:LayerIndex|Side = ? + stop:LayerIndex|Side = ?, + type:MechanicalDrill|LaserDrill = ?, + filled:True|False = ?, + tented:Side|True|False = ?, + via-in-pad:True|False = ? + backdrill:True|False = ? + ) -> Seq : + + val vs = vias(f) + val checks = [ + {check-layer(f, _0, start, stop)}, + {check-type(_0, type)}, + {check-filled(_0, filled)} + {check-tented(_0, tented)} + {check-via-in-pad(_0, via-in-pad)} + {check-backdrill(f, _0, backdrill)} + ] + for v in vs seq?: + val ret = for ch-func in checks all?: + ch-func(v) + if ret: One(v) + else: None() diff --git a/src/layerstack.stanza b/src/layerstack.stanza index 0a5d2419..0dfa06d9 100644 --- a/src/layerstack.stanza +++ b/src/layerstack.stanza @@ -348,6 +348,43 @@ doc:\ public defn conductors (ls:LayerStack) -> Tuple : to-tuple $ filter({material(_) is ConductorMaterial}, layers(ls)) +doc: \ +Check if a layer id refers to a valid copper layer in this stackup + +@param ls PCB Stackup Generator +@param l Index into the copper layers. If `Int`, this is a simple +zero-based index starting from the `Top` layer and working to the +`Bottom` layer. + +public defn is-valid-copper-layer? (ls:LayerStack, l:Int|LayerIndex) -> True|False : + val ly-id = match(l): + (i:Int): i + (li:LayerIndex): layer-num(ls, li) + ly-id >= 0 and ly-id < get-conductor-count(ls) + + +doc: \ +Convert the copper layers into a tuple of `LayerIndex` objects +@param ln Layer Stack Generator +@param omits A collection of copper layers that we don't want to +include in the generated output. Any value in this collection that matches a +copper layer will cause that `LayerIndex` to be skipped. +An `Int` value in this list indicates a simple zero-based index into the copper layers +starting at `Top` and going to `Bottom` + +public defn conductors-by-index (ls:LayerStack -- omits:Collection = []) -> Tuple : + for o in omits do: + if not is-valid-copper-layer?(ls, o): + throw $ ValueError("Invalid Omit Layer '%_' - Not Valid in this Stackup: %_" % [o, ls]) + + to-tuple $ for (c in conductors(ls), i in 0 to false) seq?: + val skip = for o in omits any?: + layer-num(ls, o) == i + if not skip: + One $ LayerIndex(i, Top) + else: + None() + doc: \ Add one or more layers to the top of the board stackup. @@ -517,6 +554,59 @@ public defn get-conductor (ls:LayerStack, index:Int) -> [Maybe, Layer (cu-index:Int): [get-pre-dielectric(cu-index), l-set[cu-index], get-post-dielectric(cu-index)] + +doc: \ +Convert a LayerIndex to a Copper Layer Index + +@param ls PCB Stackup Generator - This provides the number +of copper layers in the stackup. +@param l Which layer in the stackup we are referring to. +@return A zero-indexed value where `Top` is 0 and `Bottom` +is `N-1` where `N` is the number of layers in the stackup. +@throws ValueError If `l` is invalid for this stackup - ie, if the +layer is outside of the available copper layers of the design. + +public defn layer-num (ls:LayerStack, l:LayerIndex|Int) -> Int : + val cnt = get-conductor-count(ls) + is-valid-copper-layer?(ls, l) + match(l): + (i:Int): i + (li:LayerIndex): layer-num(li, cnt) + +doc: \ +Compare Two LayerIndex given a PCB stackup + +@param ls PCB Stackup Generator - This provides the number +of copper layers in the stackup. +@return This function returns zero if the layer indices match +This function returns a value < 0 if `a` is closer to the `Top` than `b` +This function returns a value > 0 if `a` is closer to the `Bottom` than `b` + +public defn compare-layer (ls:LayerStack, a:LayerIndex, b:LayerIndex) -> Int : + val a-num = layer-num(ls, a) + val b-num = layer-num(ls, b) + ; I'm mapping +1 to Lower in the stack (ie, Bottom) + ; I'm mapping -1 to Higher in the stack (ie, Top) + compare(a-num, b-num) + +doc: \ +Check if a given copper layer is within a range of layers + +This is an inclusive check - so if `a` == `start` or `a` == `end` - +then this check will return True. + +@param ls PCB Stackup Generator - This provides the number +of copper layers in the stackup. +@param a Copper Layer to compare against the `start` and `end` range. +@param start Start of the copper layer range. +@param end End of the copper layer range. +@return True if `a` is on or between start & end. False if outside that range. + +public defn in-range? (ls:LayerStack, a:LayerIndex, start:LayerIndex, end:LayerIndex) -> True|False : + val sc = compare-layer(ls, a, start) + val ec = compare-layer(ls, a, end) + sc >= 0 and ec <= 0 + defn make-name (x:LayerStack) : ; I can't use the `name` string in the pcb-stackup definition ; because the macro overrides it.