Source code for jitxexamples.components.crystals.seiko_epson_TSX_3225

"""
Seiko Epson TSX-3225 Crystal Units
==================================

One component class for the TSX-3225 series: 16 MHz to 50 MHz fundamental
crystal units in a 3.2 mm x 2.5 mm x 0.6 mm four-pad package, from the Epson
FA-238V / FA-238 / TSX-3225 data sheet.

The data sheet asks for the load capacitance to be specified and lists the
frequency tolerance and frequency versus temperature characteristics as
per-order requirements, so the series has no ordering code that can be built
from those fields. Each orderable unit is instead a product number. The data
sheet prints it as ``X1E000021xxxx17``; the catalog numbers used here are the
thirteen-character ``X1E000021xxxx`` and omit that two-digit ``17`` suffix.
``TSX3225`` takes the specification fields as constructor arguments and looks
the product number up in ``seiko_epson_TSX_3225.csv``, the TSX-3225 rows of
Epson's product-search catalog
(https://download.epsondevice.com/td/ps/xtal_mhz.json). A combination the
catalog does not list raises ``ValueError``; a combination it lists under more
than one product number needs ``part_number=`` to choose one.

Pads 1 and 3 are the crystal; pads 2 and 4 are connected to the cover and go to
ground.
"""

import csv
import math
from dataclasses import dataclass
from functools import cache, partial
from importlib.resources import files

import eseries
import jitx
from jitx.anchor import Anchor
from jitx.component import Component
from jitx.container import inline
from jitx.decorators import late
from jitx.feature import Courtyard, Custom, Paste, Silkscreen, Soldermask
from jitx.landpattern import Landpattern, Pad, PadMapping
from jitx.net import Net, Port, ShortTrace
from jitx.shapes.composites import rectangle
from jitx.shapes.primitive import Arc, ArcPolyline, Polyline, Text
from jitx.symbol import Direction, Pin, Symbol, SymbolMapping
from jitx.units import F
from jitxlib.parts import Capacitor


[docs] class CrystalComponent(Component): OSC1: Port = Port() OSC2: Port = Port()
[docs] class CrystalResonator(CrystalComponent): """A two-terminal crystal with the electrical parameters a load-capacitor calculation needs. ``shunt_capacitance`` and ``motional_capacitance`` are ``None`` when the manufacturer does not publish them.""" load_capacitance: float shunt_capacitance: float | None motional_capacitance: float | None esr: float frequency: float frequency_tolerance: float max_drive_level: float def __init__( self, *, load_capacitance: float, shunt_capacitance: float | None, motional_capacitance: float | None, esr: float, frequency: float, frequency_tolerance: float, max_drive_level: float, ) -> None: self.load_capacitance = load_capacitance self.shunt_capacitance = shunt_capacitance self.motional_capacitance = motional_capacitance self.esr = esr self.frequency = frequency self.frequency_tolerance = frequency_tolerance self.max_drive_level = max_drive_level @late def __post_init_checks(self) -> None: for fieldname in [ "load_capacitance", "shunt_capacitance", "motional_capacitance", "esr", "frequency", "frequency_tolerance", "max_drive_level", ]: if not hasattr(self, fieldname): raise ValueError(f"{fieldname} is required")
[docs] def add_load_caps( self, gnd: Net, stray_capacitance: float = 5.0e-12 ) -> tuple[Capacitor, Capacitor]: """Add a C0G load capacitor for each crystal pin to the current circuit, sized to the nearest E24 value of ``2 * (load_capacitance - stray_capacitance)``, and return both. Each capacitor's ``p1`` joins the crystal pin it serves through ``ShortTrace(cap.p1, <OSC1 or OSC2>)``, so placement and routing keep it close to that pin, and its ``p2`` joins ``gnd``. The returned capacitors already belong to the current circuit. Read them, but do not assign them to an attribute of the circuit as well: each would then be registered twice, and translating the design fails with "Child object ... encountered multiple times". """ capacitance = eseries.find_nearest( eseries.E24, 2.0 * (self.load_capacitance - stray_capacitance) ) cap_1 = Capacitor( capacitance=(capacitance) * F, temperature_coefficient_code="C0G" ) cap_2 = Capacitor( capacitance=(capacitance) * F, temperature_coefficient_code="C0G" ) jitx.current.circuit += [ cap_1, cap_2, ShortTrace(cap_1.p1, self.OSC1), cap_1.p2 + gnd, ShortTrace(cap_2.p1, self.OSC2), cap_2.p2 + gnd, ] return cap_1, cap_2
[docs] @dataclass(frozen=True) class CatalogEntry: """One row of ``seiko_epson_TSX_3225.csv``, in the catalog's units.""" part_number: str frequency_mhz: float load_capacitance_pf: float tolerance_ppm: float stability_ppm: float temperature_min_c: float temperature_max_c: float esr_max_ohm: float drive_level_max_uw: float
[docs] @cache def catalog() -> tuple[CatalogEntry, ...]: """The TSX-3225 product numbers Epson lists, with their specifications.""" text = ( files("jitxexamples.components.crystals") / "seiko_epson_TSX_3225.csv" ).read_text() return tuple( CatalogEntry( part_number=row["part_number"], frequency_mhz=float(row["frequency_mhz"]), load_capacitance_pf=float(row["load_capacitance_pf"]), tolerance_ppm=float(row["tolerance_ppm"]), stability_ppm=float(row["stability_ppm"]), temperature_min_c=float(row["temperature_min_c"]), temperature_max_c=float(row["temperature_max_c"]), esr_max_ohm=float(row["esr_max_ohm"]), drive_level_max_uw=float(row["drive_level_max_uw"]), ) for row in csv.DictReader(text.splitlines()) )
def _close(a: float, b: float) -> bool: """Equal to the six decimals the catalog prints, in the catalog's units.""" return math.isclose(a, b, rel_tol=0.0, abs_tol=1e-6)
[docs] class RectangleSMDPad(Pad): shape = rectangle(1.4, 1.15) soldermask = Soldermask(rectangle(1.45, 1.2)) paste = Paste(rectangle(1.45, 1.2))
Fab = partial(Custom, name="Fab")
[docs] class TSX3225Package(CrystalResonator): """The pins, symbol and recommended footprint every TSX-3225 unit shares. Pads 1 and 3 are the crystal (``OSC1``, ``OSC2``); pads 2 and 4 are connected to the cover (``GND1``, ``GND0``). A subclass supplies the electrical parameters, the part number and the data sheet. """ manufacturer = "Seiko Epson" reference_designator_prefix = "X" GND0 = Port() GND1 = Port() @inline class symbol(Symbol): pin_name_size = 1 pad_name_size = 1 GND0 = Pin(at=(-4, 2), direction=Direction.Left, length=2) GND1 = Pin(at=(4, -2), direction=Direction.Right, length=2) OSC1 = Pin(at=(-4, -2), direction=Direction.Left, length=2) OSC2 = Pin(at=(4, 2), direction=Direction.Right, length=2) value = Text(">VALUE", 0.55, anchor=Anchor.C).at(0, 2.78) reference = Text(">REF", 0.55, anchor=Anchor.C).at(0, 3.57) foreground = [ rectangle(8, 8), Polyline(0.2, [(-1, -0.8), (-1, 0.8)]), Polyline(0.2, [(0.6, -1.4), (-0.6, -1.4)]), Polyline(0.2, [(1, -0.8), (1, 0.8)]), Polyline(0.2, [(-0.6, -1.4), (-0.6, 1.4), (0.6, 1.4), (0.6, -1.4)]), Polyline(0.2, [(4, 2), (2, 2), (2, 0), (1.2, 0)]), Polyline(0.2, [(-4, -2), (-2, -2), (-2, 0), (-1.2, 0)]), Polyline(0.2, [(1, -1.4), (1, 1.4)]), Polyline(0.2, [(-1, -1.4), (-1, 1.4)]), Polyline( 0.2, [(0.6, 1.4), (0.6, -1.4), (-0.6, -1.4), (-0.6, 1.4), (0.6, 1.4)] ), ] @inline class landpattern(Landpattern): # The data sheet's recommended footprint: 1.4 mm x 1.15 mm pads on # 2.2 mm by 1.6 mm centers. p = { 1: RectangleSMDPad().at(-1.1, -0.8), 2: RectangleSMDPad().at(1.1, -0.8), 3: RectangleSMDPad().at(1.1, 0.8), 4: RectangleSMDPad().at(-1.1, 0.8), } labels = [ Silkscreen(Text(">REF", 0.5, Anchor.W).at(-0.75, 3.575)), Fab(Text(">VALUE", 0.5, Anchor.W).at(-0.75, 2.575)), ] silkscreens = [ Silkscreen( Polyline( 0.152, [(-2, -1.5), (-2, 1.5), (2, 1.5), (2, -1.5), (-2, -1.5)], ) ), Silkscreen(Polyline(0.152, [(-2.1, -0.1), (-2.1, -1.6), (-0.1, -1.6)])), ] fab_drawings = [ Fab(ArcPolyline(0.06, [Arc((-1.486, -1.136), 0.03, 0, -360)])), Fab(ArcPolyline(0.36, [Arc((-1.029, -0.775), 0.18, 0, -360)])), ] courtyard = Courtyard(rectangle(4, 3)) def __init__( self, *, load_capacitance: float, shunt_capacitance: float | None, motional_capacitance: float | None, esr: float, frequency: float, frequency_tolerance: float, max_drive_level: float, ) -> None: super().__init__( load_capacitance=load_capacitance, shunt_capacitance=shunt_capacitance, motional_capacitance=motional_capacitance, esr=esr, frequency=frequency, frequency_tolerance=frequency_tolerance, max_drive_level=max_drive_level, ) self.mappings = [ PadMapping( { self.GND0: self.landpattern.p[4], self.GND1: self.landpattern.p[2], self.OSC1: self.landpattern.p[1], self.OSC2: self.landpattern.p[3], } ), SymbolMapping( { self.GND0: self.symbol.GND0, self.GND1: self.symbol.GND1, self.OSC1: self.symbol.OSC1, self.OSC2: self.symbol.OSC2, } ), ]
def _describe(entry: CatalogEntry) -> str: """One catalog row's specification, each field labeled.""" return ( f"CL {entry.load_capacitance_pf:g} pF, " f"tolerance +/-{entry.tolerance_ppm:g} ppm at +25 C, " f"stability +/-{entry.stability_ppm:g} ppm over " f"{entry.temperature_min_c:g} to {entry.temperature_max_c:g} C" )
[docs] class TSX3225(TSX3225Package): """Seiko Epson TSX-3225 crystal unit, 3.2 mm x 2.5 mm, selected by its specification. Args: frequency: nominal frequency in Hz. load_capacitance: load capacitance CL in farads. frequency_tolerance: frequency tolerance at +25 C, as a fraction (``10e-6`` for +/-10 ppm). frequency_stability: frequency versus temperature over ``temperature_range``, as a fraction. temperature_range: the range ``frequency_stability`` holds over, as (minimum, maximum) in degrees Celsius. part_number: the product number to use when the catalog lists the specification under more than one. It must be one of those. Raises: ValueError: the catalog lists no product number for the specification; lists several and ``part_number`` is not given; or ``part_number`` is not one the catalog lists for it. """ datasheet = "https://download.epsondevice.com/td/pdf/brief/TSX-3225_en.pdf" frequency_stability: float temperature_range: tuple[float, float] def __init__( self, *, frequency: float, load_capacitance: float, frequency_tolerance: float, frequency_stability: float, temperature_range: tuple[float, float], part_number: str | None = None, ) -> None: low, high = temperature_range mhz = frequency / 1e6 asked = ( f"a TSX-3225 at {mhz:g} MHz, CL {load_capacitance / 1e-12:g} pF, " f"tolerance +/-{frequency_tolerance / 1e-6:g} ppm at +25 C, " f"stability +/-{frequency_stability / 1e-6:g} ppm over {low:g} to " f"{high:g} C" ) matches = [ entry for entry in catalog() if _close(entry.frequency_mhz, mhz) and _close(entry.load_capacitance_pf, load_capacitance / 1e-12) and _close(entry.tolerance_ppm, frequency_tolerance / 1e-6) and _close(entry.stability_ppm, frequency_stability / 1e-6) and _close(entry.temperature_min_c, low) and _close(entry.temperature_max_c, high) ] if not matches: listed = sorted( {_describe(e) for e in catalog() if _close(e.frequency_mhz, mhz)} ) raise ValueError( f"Epson lists no product number for {asked}. " f"Listed at {mhz:g} MHz: {'; '.join(listed) or 'none'}" ) numbers = [entry.part_number for entry in matches] if part_number is not None: if part_number not in numbers: raise ValueError( f"{part_number} is not a product number Epson lists for " f"{asked}; it lists {', '.join(numbers)}" ) matches = [e for e in matches if e.part_number == part_number] if len(matches) > 1: raise ValueError( f"Epson lists several product numbers for {asked}: " f"{', '.join(numbers)}. Pass part_number= to choose one." ) entry = matches[0] super().__init__( load_capacitance=load_capacitance, shunt_capacitance=None, motional_capacitance=None, esr=entry.esr_max_ohm, frequency=frequency, frequency_tolerance=frequency_tolerance, max_drive_level=entry.drive_level_max_uw * 1e-6, ) self.frequency_stability = frequency_stability self.temperature_range = (low, high) self.mpn = entry.part_number
[docs] class TSX_3225_32_0000MF10Z_W6(TSX3225Package): """32 MHz +/-10 ppm crystal, 12 pF, 40 ohms, ordered by its distributor code. Its values come from the distributor's data sheet. No source here ties the distributor code to one of the catalog's product numbers, so the unit takes no catalog row and has no ``frequency_stability`` or ``temperature_range``.""" mpn = "TSX-3225 32.0000MF10Z-W6" datasheet = "https://datasheet.lcsc.com/lcsc/2201301130_Seiko-Epson-TSX-3225-32-0000MF10Z-W6_C1986412.pdf" def __init__(self) -> None: super().__init__( load_capacitance=12e-12, shunt_capacitance=1.31e-12, motional_capacitance=4.41e-15, esr=40, frequency=32.0e6, frequency_tolerance=10.0e-6, max_drive_level=100.0e-6, )
Device: type[TSX_3225_32_0000MF10Z_W6] = TSX_3225_32_0000MF10Z_W6