"""Diode pcell generators for sky130.
Provides N+/P-well and P+/N-well diode parametric cells matching the sky130
Magic VLSI reference geometry, including inline guard ring tap geometry with
licon contacts, mcon, met1, and implant/well layers.
"""
import gdsfactory as gf
from sky130.layers import LAYER
from sky130.pcells.contact import contact_array
# ---------------------------------------------------------------------------
# Geometry constants (um) derived from Magic reference
# ---------------------------------------------------------------------------
_IMPLANT_ENC = 0.125 # NSDM / PSDM enclosure beyond diffusion or tap
_NWELL_ENC = 0.18 # N-well enclosure beyond inner guard ring tap
_RING_SPACING = 0.34 # gap from diff edge (or nwell edge) to guard ring inner edge
_RING_WIDTH = 0.17 # width of each guard ring tap segment
_LICON_SIZE = 0.17 # licon contact size
_LICON_SPACE = 0.17 # licon contact spacing
_LICON_ENC = 0.06 # licon enclosure within diff
_MCON_SIZE = 0.17 # mcon contact size
_MCON_SPACE = 0.19 # mcon contact spacing
_MCON_ENC = 0.06 # mcon enclosure within diff area
_RING_LICON_ENC_BASE = 0.31 # licon enclosure from inner edge of guard ring
# Horizontal segments (with corner overlap) add _RING_WIDTH to this base.
_RING_LICON_ENC_HORIZ = _RING_LICON_ENC_BASE + _RING_WIDTH # 0.48
_RING_LICON_ENC_VERT = _RING_LICON_ENC_BASE # 0.31
def _add_box(c: gf.Component, layer, x0: float, y0: float, x1: float, y1: float):
"""Add a rectangle to *c* given (x0, y0) to (x1, y1) corners."""
c.add_polygon([(x0, y0), (x1, y0), (x1, y1), (x0, y1)], layer=layer)
def _contact_array_eps(
width: float,
height: float,
contact_layer,
contact_size: float,
contact_spacing: float,
enc_x: float,
enc_y: float,
) -> gf.Component:
"""Contact array with epsilon tolerance to avoid floating-point floor errors."""
return contact_array(
width=width,
height=height,
contact_layer=contact_layer,
contact_size=(contact_size, contact_size),
contact_spacing=(contact_spacing, contact_spacing),
enclosure=(enc_x, enc_y),
)
def _guard_ring_contacts(
c: gf.Component,
seg_x: float,
seg_y: float,
seg_w: float,
seg_h: float,
):
"""Place licon contacts on one guard ring tap segment.
Horizontal segments (top/bottom, wider than tall) have corner overlap so
they use the larger enclosure (_RING_LICON_ENC_HORIZ = 0.48) in the long
direction and zero in the short direction.
Vertical segments (left/right, taller than wide) have no corner overlap so
they use the smaller enclosure (_RING_LICON_ENC_VERT = 0.31) in the long
direction and zero in the short direction.
"""
if seg_w >= seg_h:
# horizontal segment (top / bottom)
enc_x = _RING_LICON_ENC_HORIZ
enc_y = 0.0
else:
# vertical segment (left / right)
enc_x = 0.0
enc_y = _RING_LICON_ENC_VERT
cont = c.add_ref(
_contact_array_eps(
width=seg_w,
height=seg_h,
contact_layer=LAYER.licon1drawing,
contact_size=_LICON_SIZE,
contact_spacing=_LICON_SPACE,
enc_x=enc_x,
enc_y=enc_y,
)
)
cont.move((seg_x, seg_y))
def _draw_ring(
c: gf.Component,
inner_half_x: float,
inner_half_y: float,
ring_width: float,
tap_layer,
implant_layer,
li1_layer,
with_licon: bool = True,
):
"""Draw one rectangular guard ring centered at origin.
The ring inner edge is at +/-*inner_half_x* in X and +/-*inner_half_y*
in Y. Ring outer edge is inner + *ring_width*. Tap, implant, li1, and
licon contacts are placed on all four segments.
Returns (outer_half_x, outer_half_y) for downstream geometry.
"""
ihx = inner_half_x
ihy = inner_half_y
rw = ring_width
ohx = ihx + rw # outer half-extent in X
ohy = ihy + rw # outer half-extent in Y
# Four tap / li1 segments (top, bottom full width; left, right inner height).
segs = [
# (x0, y0, x1, y1)
(-ohx, ihy, ohx, ohy), # top
(-ohx, -ohy, ohx, -ihy), # bottom
(-ohx, -ihy, -ihx, ihy), # left
(ihx, -ihy, ohx, ihy), # right
]
for x0, y0, x1, y1 in segs:
_add_box(c, tap_layer, x0, y0, x1, y1)
_add_box(c, li1_layer, x0, y0, x1, y1)
# Implant: 4 segments extending _IMPLANT_ENC beyond tap
ie = _IMPLANT_ENC
imp_ohx = ohx + ie
imp_ohy = ohy + ie
imp_ihx = ihx - ie
imp_ihy = ihy - ie
imp_segs = [
(-imp_ohx, imp_ihy, imp_ohx, imp_ohy), # top
(-imp_ohx, -imp_ohy, imp_ohx, -imp_ihy), # bottom
(-imp_ohx, -imp_ihy, -imp_ihx, imp_ihy), # left
(imp_ihx, -imp_ihy, imp_ohx, imp_ihy), # right
]
for x0, y0, x1, y1 in imp_segs:
_add_box(c, implant_layer, x0, y0, x1, y1)
# Licon contacts on each segment
if with_licon:
for x0, y0, x1, y1 in segs:
_guard_ring_contacts(c, x0, y0, x1 - x0, y1 - y0)
return ohx, ohy
# ---------------------------------------------------------------------------
# pw2nd (N+/P-well) diode
# ---------------------------------------------------------------------------
[docs]
@gf.cell
def sky130_fd_pr__diode_pw2nd_05v5(
diode_width: float = 0.45,
diode_length: float = 0.45,
) -> gf.Component:
"""Return an N+/P-well diode (cathode = N+ diffusion in P-well substrate).
Geometry centered at origin, matching Magic VLSI reference layout:
- diff + NSDM implant (cathode)
- P+ tap guard ring with PSDM implant (anode / substrate)
- licon on diff and guard ring, mcon + met1 on diff
- areaid_diode marker
Ports:
CATHODE -- on met1drawing over diffusion.
ANODE -- on li1drawing at guard ring bottom.
Args:
diode_width: width of the diode diffusion in um.
diode_length: length (height) of the diode diffusion in um.
.. plot::
:include-source:
import sky130
c = sky130.pcells.sky130_fd_pr__diode_pw2nd_05v5()
c.plot()
"""
c = gf.Component()
hw = diode_width / 2 # half-width
hl = diode_length / 2 # half-length
# --- Diffusion ---
_add_box(c, LAYER.diffdrawing, -hw, -hl, hw, hl)
# --- areaid_diode ---
_add_box(c, LAYER.areaiddiode, -hw, -hl, hw, hl)
# --- NSDM implant over diff ---
_add_box(
c,
LAYER.nsdmdrawing,
-hw - _IMPLANT_ENC,
-hl - _IMPLANT_ENC,
hw + _IMPLANT_ENC,
hl + _IMPLANT_ENC,
)
# --- Licon contacts on diff ---
licon_ref = c.add_ref(
_contact_array_eps(
width=diode_width,
height=diode_length,
contact_layer=LAYER.licon1drawing,
contact_size=_LICON_SIZE,
contact_spacing=_LICON_SPACE,
enc_x=_LICON_ENC,
enc_y=_LICON_ENC,
)
)
licon_ref.move((-hw, -hl))
# --- Li1 pad on diff ---
# When W >= L the wider dimension is X; when W < L the wider is Y.
if diode_width >= diode_length:
li1_hx = hw + 0.02
li1_hy = hl - 0.06
else:
li1_hx = hw - 0.06
li1_hy = hl + 0.02
_add_box(c, LAYER.li1drawing, -li1_hx, -li1_hy, li1_hx, li1_hy)
# --- Mcon contacts on diff ---
mcon_ref = c.add_ref(
_contact_array_eps(
width=diode_width,
height=diode_length,
contact_layer=LAYER.mcondrawing,
contact_size=_MCON_SIZE,
contact_spacing=_MCON_SPACE,
enc_x=_MCON_ENC,
enc_y=_MCON_ENC,
)
)
mcon_ref.move((-hw, -hl))
# --- Met1 pad on diff ---
if diode_width >= diode_length:
met1_hx = hw
met1_hy = hl - 0.03
else:
met1_hx = hw - 0.03
met1_hy = hl
_add_box(c, LAYER.met1drawing, -met1_hx, -met1_hy, met1_hx, met1_hy)
# --- P+ guard ring (pwell / substrate ring) ---
ring_ih_x = hw + _RING_SPACING
ring_ih_y = hl + _RING_SPACING
ring_oh_x, ring_oh_y = _draw_ring(
c,
ring_ih_x,
ring_ih_y,
_RING_WIDTH,
tap_layer=LAYER.tapdrawing,
implant_layer=LAYER.psdmdrawing,
li1_layer=LAYER.li1drawing,
)
# --- Boundary marker (235,4) ---
bnd_x = ring_ih_x + ring_oh_x
bnd_y = ring_ih_y + ring_oh_y
_add_box(c, LAYER.prBoundaryboundary, -bnd_x, -bnd_y, bnd_x, bnd_y)
# --- Labels ---
c.add_label("D1", position=(0.0, 0.0), layer=LAYER.li1label)
d2_y = -(ring_ih_y + ring_oh_y) / 2
c.add_label("D2", position=(0.0, d2_y), layer=LAYER.li1label)
# --- Ports ---
c.add_port(
name="CATHODE",
center=(0.0, 0.0),
width=diode_width,
orientation=90,
layer=LAYER.met1drawing,
port_type="electrical",
)
c.add_port(
name="ANODE",
center=(0.0, d2_y),
width=2 * ring_oh_x,
orientation=270,
layer=LAYER.li1drawing,
port_type="electrical",
)
return c
# ---------------------------------------------------------------------------
# pd2nw (P+/N-well) diode
# ---------------------------------------------------------------------------
[docs]
@gf.cell
def sky130_fd_pr__diode_pd2nw_05v5(
diode_width: float = 0.45,
diode_length: float = 0.45,
) -> gf.Component:
"""Return a P+/N-well diode (anode = P+ diffusion in N-well).
Geometry centered at origin, matching Magic VLSI reference layout:
- diff + PSDM implant (anode) inside N-well
- Inner N+ tap guard ring with NSDM implant (cathode / N-well contact)
- N-well covering diff + inner ring + 0.18 um extension
- Outer P+ tap guard ring with PSDM implant (substrate ring)
- licon on diff and both guard rings, mcon + met1 on diff
- areaid_diode marker
Ports:
ANODE -- on met1drawing over diffusion.
CATHODE -- on li1drawing at inner guard ring bottom.
Args:
diode_width: width of the diode diffusion in um.
diode_length: length (height) of the diode diffusion in um.
.. plot::
:include-source:
import sky130
c = sky130.pcells.sky130_fd_pr__diode_pd2nw_05v5()
c.plot()
"""
c = gf.Component()
hw = diode_width / 2
hl = diode_length / 2
# --- Diffusion ---
_add_box(c, LAYER.diffdrawing, -hw, -hl, hw, hl)
# --- areaid_diode ---
_add_box(c, LAYER.areaiddiode, -hw, -hl, hw, hl)
# --- PSDM implant over diff (P+ diffusion) ---
_add_box(
c,
LAYER.psdmdrawing,
-hw - _IMPLANT_ENC,
-hl - _IMPLANT_ENC,
hw + _IMPLANT_ENC,
hl + _IMPLANT_ENC,
)
# --- Licon contacts on diff ---
licon_ref = c.add_ref(
_contact_array_eps(
width=diode_width,
height=diode_length,
contact_layer=LAYER.licon1drawing,
contact_size=_LICON_SIZE,
contact_spacing=_LICON_SPACE,
enc_x=_LICON_ENC,
enc_y=_LICON_ENC,
)
)
licon_ref.move((-hw, -hl))
# --- Li1 pad on diff ---
if diode_width >= diode_length:
li1_hx = hw + 0.02
li1_hy = hl - 0.06
else:
li1_hx = hw - 0.06
li1_hy = hl + 0.02
_add_box(c, LAYER.li1drawing, -li1_hx, -li1_hy, li1_hx, li1_hy)
# --- Mcon contacts on diff ---
mcon_ref = c.add_ref(
_contact_array_eps(
width=diode_width,
height=diode_length,
contact_layer=LAYER.mcondrawing,
contact_size=_MCON_SIZE,
contact_spacing=_MCON_SPACE,
enc_x=_MCON_ENC,
enc_y=_MCON_ENC,
)
)
mcon_ref.move((-hw, -hl))
# --- Met1 pad on diff ---
if diode_width >= diode_length:
met1_hx = hw
met1_hy = hl - 0.03
else:
met1_hx = hw - 0.03
met1_hy = hl
_add_box(c, LAYER.met1drawing, -met1_hx, -met1_hy, met1_hx, met1_hy)
# --- Inner guard ring: N+ tap ring inside N-well ---
inner_ih_x = hw + _RING_SPACING
inner_ih_y = hl + _RING_SPACING
inner_oh_x, inner_oh_y = _draw_ring(
c,
inner_ih_x,
inner_ih_y,
_RING_WIDTH,
tap_layer=LAYER.tapdrawing,
implant_layer=LAYER.nsdmdrawing,
li1_layer=LAYER.li1drawing,
)
# --- N-well: covers diff + inner ring + 0.18 extension ---
nwell_hx = inner_oh_x + _NWELL_ENC
nwell_hy = inner_oh_y + _NWELL_ENC
_add_box(c, LAYER.nwelldrawing, -nwell_hx, -nwell_hy, nwell_hx, nwell_hy)
# --- Outer guard ring: P+ tap ring in substrate ---
outer_ih_x = nwell_hx + _RING_SPACING
outer_ih_y = nwell_hy + _RING_SPACING
_draw_ring(
c,
outer_ih_x,
outer_ih_y,
_RING_WIDTH,
tap_layer=LAYER.tapdrawing,
implant_layer=LAYER.psdmdrawing,
li1_layer=LAYER.li1drawing,
)
# --- Boundary marker (235,4) — based on inner ring geometry ---
bnd_x = inner_ih_x + inner_oh_x
bnd_y = inner_ih_y + inner_oh_y
_add_box(c, LAYER.prBoundaryboundary, -bnd_x, -bnd_y, bnd_x, bnd_y)
# --- Labels ---
c.add_label("D1", position=(0.0, 0.0), layer=LAYER.li1label)
d2_y = -(inner_ih_y + inner_oh_y) / 2
c.add_label("D2", position=(0.0, d2_y), layer=LAYER.li1label)
# --- Ports ---
c.add_port(
name="ANODE",
center=(0.0, 0.0),
width=diode_width,
orientation=90,
layer=LAYER.met1drawing,
port_type="electrical",
)
c.add_port(
name="CATHODE",
center=(0.0, d2_y),
width=2 * inner_oh_x,
orientation=270,
layer=LAYER.li1drawing,
port_type="electrical",
)
return c
if __name__ == "__main__":
c = sky130_fd_pr__diode_pw2nd_05v5()
c.show()